diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc82b018..6102840ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Change Log -## 0.3.28-SNAPSHOT +## 0.3.34-SNAPSHOT +* fix(GH-193): Fixed error in GQL Queries when `total` or `pages` is included with condition `where: NOT_EXISTS` or `EXISTS` (#201) [40a0e2d](https://github.com/introproventures/graphql-jpa-query/commit/40a0e2d844854f8888e3502d1d6434db9cb1dd7e) +* fix(GH-198): adedd support for fetching optional element collections elements (#200) [5d99c3b](https://github.com/introproventures/graphql-jpa-query/commit/5d99c3b4629521ebee2788c6b877250c279c8bf2) +* fix: Added support for binding orderBy argument as a variable (#195) [2a01382](https://github.com/introproventures/graphql-jpa-query/commit/2a0138237cb639427f169b18f6b6f159c430afac) +* fix: removed EntityManager.clear() from GraphQLJpaQueryDataFetcher (#192) [ae25aba](https://github.com/introproventures/graphql-jpa-query/commit/ae25aba8cbcf4397f2f0fb2c79fa2a23d9697cb6) +* chore: remove obsolete JpaQueryBuilder class (#190) [ae9ce62](https://github.com/introproventures/graphql-jpa-query/commit/ae9ce6203c6d1f3c9448dddfa2405c61057f0035) +* fix: switch off lower(COLUMN_NAME) decoration on String equality check (#186) [9445f89](https://github.com/introproventures/graphql-jpa-query/commit/9445f8965cb9e23665f4dc4f6ecc88833157bd5f) +* fix: coerce Zoned and Offset time to UTC [f2d4756](https://github.com/introproventures/graphql-jpa-query/commit/f2d475640aa36ecbb6592fa4e216aa645772e832) +* fix: Javadoc errors [8284cbd](https://github.com/introproventures/graphql-jpa-query/commit/8284cbd84c80092b4e506fbffc5706d0bed8133d) +* fix: support uppercase attribute names introspection (#173) [798b30d](https://github.com/introproventures/graphql-jpa-query/commit/798b30d7fd8978f2cf8819d2e52d8c355f689b8b) +* fix: add query support for entities with `@EmbeddedId` attribute (#180) [6552cb8](https://github.com/introproventures/graphql-jpa-query/commit/6552cb8b749ab5e1254b614375ad44badbc43b9d) +* fix: Error querying entity with @IdClass #176 (#179) [50df533](https://github.com/introproventures/graphql-jpa-query/commit/50df533b87cd174f3eeb45266e23adf7172327ce) +* fix: Description is not inherited from the parent class (#169) [82be5d5](https://github.com/introproventures/graphql-jpa-query/commit/82be5d5e3773923119cc549052875e6f9e3ee050) +* Add java arrays basic support (#171) [a5f72bc](https://github.com/introproventures/graphql-jpa-query/commit/a5f72bc2c359c72cf7b91d6098286a2c3a903b68) +* feat: support transient field modifier with class hierarchy introspection (#168) [81a6a19](https://github.com/introproventures/graphql-jpa-query/commit/81a6a198a1eecafb6056ed8ea9752482d26ba589) +* feat: add support for variable where criteria expressions (#162) [5385a41](https://github.com/introproventures/graphql-jpa-query/commit/5385a4147cba654b4f57f3c6fc7cb4d40009ca3a) +* Add METHOD to the @GraphQLIgnore annotation target (#165) [8028d12](https://github.com/introproventures/graphql-jpa-query/commit/8028d129f70f497e38463ad18fe2900072129877) +* feat: add conditional property to disable GraphQLController (#161) [4bf7596](https://github.com/introproventures/graphql-jpa-query/commit/4bf75966b31e9a7d7e931ad9fbd300fec109d02d) +* Add LocalDate/LocalDateTime/OffsetDateTime/ZonedDateTime/Instant support for query and filter (#158) [a3f7877](https://github.com/introproventures/graphql-jpa-query/commit/a3f7877b1b80fc074c4677c67511c55413172ef1) +* fix(versions): update Spring Boot to 2.1.7 (#159) [257a92d](https://github.com/introproventures/graphql-jpa-query/commit/257a92d58cd12ef67ec4134c0277967c72d8eb9e) * feat: add EXISTS/NOT_EXISTS subquery logical where criteria expressions (#151) [85d2f3a](https://github.com/introproventures/graphql-jpa-query/commit/85d2f3a7b64db6d54300f6979f872b38d1738364) * fix: correct element collection fetch sort order (#149) [5327a22](https://github.com/introproventures/graphql-jpa-query/commit/5327a226b6e86418612fbaeaf6fedc7cd3c51576) * fix: add support for nested where criteria expressions (#148) [2065101](https://github.com/introproventures/graphql-jpa-query/commit/206510146783dde2446e21f9a2c06cf61dcedf69) diff --git a/README.md b/README.md index 6d0bb9857..5b5880129 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ GraphQL Query Api for JPA Entity Models [![Try in PWD](https://cdn.rawgit.com/pl [![Maven Central](https://img.shields.io/maven-central/v/com.introproventures/graphql-jpa-query.svg)](https://mvnrepository.com/artifact/com.introproventures/graphql-jpa-query) [![Jitpack.io](https://jitpack.io/v/introproventures/graphql-jpa-query.svg)](https://jitpack.io/#introproventures/graphql-jpa-query) -GraphQL is a query language for Web APIs implemented by GraphQL Java [graphql-java 11.0](https://github.com/andimarek/graphql-java) runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. - -JPA 2.1 (Java Persistence Annotation) is Java's standard solution to bridge the gap between object-oriented domain models and relational database systems. - GraphQL JPA Query library uses JPA 2.1 specification to derive and build GraphQL Apis using GraphQL Java for your JPA Entity Java Classes. It provides a powerfull JPA Query Schema Builder to generate GraphQL Schema using JPA EntityManager Api and instruments GraphQL Schema with JPA Query Data Fetchers that transform GraphQL queries into JPA queries on the fly. -Your applications can now use GraphQL queries that smoothly follow references between JPA resources with flexible type safe criteria expressions and user-friendly SQL query syntax semantics i.e. query by page, where criteria expressions, select, order by etc. +GraphQL is a query language for Web APIs implemented by GraphQL Java [graphql-java 11.0](https://github.com/andimarek/graphql-java) runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. + +Your applications can now use GraphQL queries that smoothly follow references between JPA entities with flexible type safe criteria expressions and user-friendly SQL query syntax semantics i.e. query by page, where criteria expressions, select, order by etc. While typical REST APIs require loading from multiple URLs, GraphQL APIs get all the data your app needs in a single request. Apps using GraphQL can be quick even on slow mobile network connections. +JPA 2.1 (Java Persistence Annotation) is Java's standard solution to bridge the gap between object-oriented domain models and relational database systems. + GraphQL JPA Query creates a uniform query API across for your applications without being limited by a single data source. You can use it with multiple JPA compliant databases by instrumenting separate EntityManager for each DataSource and expose a single GraphQL Query Apis for your Web application domain using Spring Boot Auto Configuration magic. Tested using JDK Versions diff --git a/graphql-jpa-query-annotations/pom.xml b/graphql-jpa-query-annotations/pom.xml index 0300d398d..310c94a5a 100644 --- a/graphql-jpa-query-annotations/pom.xml +++ b/graphql-jpa-query-annotations/pom.xml @@ -6,7 +6,7 @@ com.introproventures graphql-jpa-query-dependencies - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-dependencies diff --git a/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLIgnore.java b/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLIgnore.java index 0a9eaf922..7003f4dbe 100644 --- a/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLIgnore.java +++ b/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLIgnore.java @@ -16,6 +16,7 @@ package com.introproventures.graphql.jpa.query.annotation; import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -28,7 +29,7 @@ * @author Igor Dianov * */ -@Target( { TYPE, FIELD }) +@Target( { TYPE, FIELD, METHOD }) @Retention(RUNTIME) public @interface GraphQLIgnore { } diff --git a/graphql-jpa-query-autoconfigure/pom.xml b/graphql-jpa-query-autoconfigure/pom.xml index d8b3ce3bd..0e47a05d5 100644 --- a/graphql-jpa-query-autoconfigure/pom.xml +++ b/graphql-jpa-query-autoconfigure/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build graphql-jpa-query-autoconfigure diff --git a/graphql-jpa-query-boot-starter/pom.xml b/graphql-jpa-query-boot-starter/pom.xml index 7ffa745c0..6560dfa4d 100644 --- a/graphql-jpa-query-boot-starter/pom.xml +++ b/graphql-jpa-query-boot-starter/pom.xml @@ -7,7 +7,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build diff --git a/graphql-jpa-query-build/pom.xml b/graphql-jpa-query-build/pom.xml index 39cddc549..2a2fca240 100644 --- a/graphql-jpa-query-build/pom.xml +++ b/graphql-jpa-query-build/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query-dependencies - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-dependencies graphql-jpa-query-build @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-dependencies - 2.1.3.RELEASE + 2.1.7.RELEASE pom import diff --git a/graphql-jpa-query-dependencies/pom.xml b/graphql-jpa-query-dependencies/pom.xml index 8af3d4e71..6709c8b19 100644 --- a/graphql-jpa-query-dependencies/pom.xml +++ b/graphql-jpa-query-dependencies/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT .. graphql-jpa-query-dependencies diff --git a/graphql-jpa-query-example-merge/pom.xml b/graphql-jpa-query-example-merge/pom.xml index 18d66fbda..cf4d61ac6 100644 --- a/graphql-jpa-query-example-merge/pom.xml +++ b/graphql-jpa-query-example-merge/pom.xml @@ -7,7 +7,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build diff --git a/graphql-jpa-query-example-merge/src/main/resources/books.sql b/graphql-jpa-query-example-merge/src/main/resources/books.sql index 928d6a3d5..50a35ec5f 100644 --- a/graphql-jpa-query-example-merge/src/main/resources/books.sql +++ b/graphql-jpa-query-example-merge/src/main/resources/books.sql @@ -7,3 +7,16 @@ insert into book (id, title, author_id, genre) values (5, 'The Cherry Orchard', insert into book (id, title, author_id, genre) values (6, 'The Seagull', 4, 'PLAY'); insert into book (id, title, author_id, genre) values (7, 'Three Sisters', 4, 'PLAY'); insert into author (id, name, genre) values (8, 'Igor Dianov', 'JAVA'); + +insert into book_tags (book_id, tags) values (2, 'war'), (2, 'piece'); +insert into book_tags (book_id, tags) values (3, 'anna'), (3, 'karenina'); +insert into book_tags (book_id, tags) values (5, 'cherry'), (5, 'orchard'); +insert into book_tags (book_id, tags) values (6, 'seagull'); +insert into book_tags (book_id, tags) values (7, 'three'), (7, 'sisters'); + +insert into author_phone_numbers(phone_number, author_id) values + ('1-123-1234', 1), + ('1-123-5678', 1), + ('4-123-1234', 4), + ('4-123-5678', 4); + \ No newline at end of file diff --git a/graphql-jpa-query-example-model-books/pom.xml b/graphql-jpa-query-example-model-books/pom.xml index 8e8d45a52..ff711c100 100644 --- a/graphql-jpa-query-example-model-books/pom.xml +++ b/graphql-jpa-query-example-model-books/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build graphql-jpa-query-example-model-books diff --git a/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java b/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java index efb530e1d..ce8045270 100644 --- a/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java +++ b/graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java @@ -17,22 +17,29 @@ package com.introproventures.graphql.jpa.query.schema.model.book; import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; +import javax.persistence.Transient; +import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreFilter; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreOrder; + import lombok.Data; import lombok.EqualsAndHashCode; @Data @Entity -@EqualsAndHashCode(exclude="author") +@EqualsAndHashCode(exclude= {"author", "tags"}) public class Book { @Id Long id; @@ -42,6 +49,10 @@ public class Book { @GraphQLIgnoreOrder @GraphQLIgnoreFilter String description; + + @ElementCollection(fetch = FetchType.LAZY) + @GraphQLDescription("A set of user-defined tags") + private Set tags = new LinkedHashSet<>(); @ManyToOne(fetch=FetchType.LAZY, optional = false) Author author; @@ -49,5 +60,11 @@ public class Book { @Enumerated(EnumType.STRING) Genre genre; - Date publicationDate; + Date publicationDate; + + @Transient + @GraphQLIgnore + public String getAuthorName(){ + return author.getName(); + } } diff --git a/graphql-jpa-query-example-model-starwars/pom.xml b/graphql-jpa-query-example-model-starwars/pom.xml index 9a91f3620..68994082c 100644 --- a/graphql-jpa-query-example-model-starwars/pom.xml +++ b/graphql-jpa-query-example-model-starwars/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build diff --git a/graphql-jpa-query-example-simple/pom.xml b/graphql-jpa-query-example-simple/pom.xml index c9a9373af..d240d315e 100644 --- a/graphql-jpa-query-example-simple/pom.xml +++ b/graphql-jpa-query-example-simple/pom.xml @@ -7,7 +7,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build diff --git a/graphql-jpa-query-graphiql/pom.xml b/graphql-jpa-query-graphiql/pom.xml index 6ace54d69..b0c5c492b 100644 --- a/graphql-jpa-query-graphiql/pom.xml +++ b/graphql-jpa-query-graphiql/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT graphql-jpa-query-graphiql \ No newline at end of file diff --git a/graphql-jpa-query-schema/pom.xml b/graphql-jpa-query-schema/pom.xml index 87d219ab8..b2fbee6df 100644 --- a/graphql-jpa-query-schema/pom.xml +++ b/graphql-jpa-query-schema/pom.xml @@ -5,7 +5,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/AnnotationDescriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/AnnotationDescriptor.java new file mode 100644 index 000000000..73db9e4a0 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/AnnotationDescriptor.java @@ -0,0 +1,115 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.Objects; + +public class AnnotationDescriptor { + + private final Annotation annotation; + + private final Class annotationType; + + private final ElementType[] elementTypes; + + private final RetentionPolicy policy; + + private final boolean isDocumented; + + private final boolean isInherited; + + public AnnotationDescriptor(A annotation) { + this.annotation = annotation; + annotationType = annotation.annotationType(); + + Target target = annotationType.getAnnotation(Target.class); + elementTypes = (target == null) ? ElementType.values() : target.value(); + + Retention retention = annotationType.getAnnotation(Retention.class); + policy = (retention == null) ? RetentionPolicy.CLASS : retention.value(); + + Documented documented = annotationType.getAnnotation(Documented.class); + isDocumented = (documented != null); + + Inherited inherited = annotationType.getAnnotation(Inherited.class); + isInherited = (inherited != null); + } + + @SuppressWarnings("unchecked") + public A getAnnotation() { + return (A) annotation; + } + + public Class getAnnotationType() { + return annotationType; + } + + public ElementType[] getElementTypes() { + return elementTypes; + } + + public RetentionPolicy getPolicy() { + return policy; + } + + public boolean isDocumented() { + return isDocumented; + } + + public boolean isInherited() { + return isInherited; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AnnotationDescriptor [annotation=") + .append(annotation) + .append(", annotationType=") + .append(annotationType) + .append(", elementTypes=") + .append(Arrays.toString(elementTypes)) + .append(", policy=") + .append(policy) + .append(", isDocumented=") + .append(isDocumented) + .append(", isInherited=") + .append(isInherited) + .append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(elementTypes); + result = prime * result + Objects.hash(annotation, annotationType, isDocumented, isInherited, policy); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AnnotationDescriptor other = (AnnotationDescriptor) obj; + return Objects.equals(annotation, other.annotation) + && Objects.equals(annotationType, other.annotationType) + && Arrays.equals(elementTypes, other.elementTypes) + && isDocumented == other.isDocumented + && isInherited == other.isInherited + && policy == other.policy; + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Annotations.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Annotations.java new file mode 100644 index 000000000..6c68267f8 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Annotations.java @@ -0,0 +1,100 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public class Annotations { + + protected final AnnotatedElement annotatedElement; + + protected final Map, AnnotationDescriptor> annotationsMap; + + // cache + private AnnotationDescriptor[] allAnnotations; + + public Annotations(AnnotatedElement annotatedElement) { + this.annotatedElement = annotatedElement; + this.annotationsMap = inspectAnnotations(); + } + + private Map, AnnotationDescriptor> inspectAnnotations() { + + Annotation[] annotations = ReflectionUtil.getAnnotation(annotatedElement); + if (ArrayUtil.isEmpty(annotations)) { + return null; + } + + Map, AnnotationDescriptor> map = new LinkedHashMap<>(annotations.length); + + for (Annotation annotation : annotations) { + map.put(annotation.annotationType(), new AnnotationDescriptor(annotation)); + } + + return map; + } + + public AnnotationDescriptor getAnnotationDescriptor(Class clazz) { + if (annotationsMap == null) { + return null; + } + + return annotationsMap.get(clazz); + } + + public AnnotationDescriptor[] getAllAnnotationDescriptors() { + if (annotationsMap == null) { + return null; + } + + if (allAnnotations == null) { + AnnotationDescriptor[] allAnnotations = new AnnotationDescriptor[annotationsMap.size()]; + + int index = 0; + for (AnnotationDescriptor annotationDescriptor : annotationsMap.values()) { + allAnnotations[index] = annotationDescriptor; + index++; + } + + Arrays.sort(allAnnotations, new Comparator() { + @Override + public int compare(AnnotationDescriptor ad1, AnnotationDescriptor ad2) { + return ad1.getClass().getName().compareTo(ad2.getClass().getName()); + } + }); + + this.allAnnotations = allAnnotations; + } + + return allAnnotations; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Annotations [annotatedElement=").append(annotatedElement).append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(annotatedElement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Annotations other = (Annotations) obj; + return Objects.equals(annotatedElement, other.annotatedElement); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ArrayUtil.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ArrayUtil.java new file mode 100644 index 000000000..7c5a70d33 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ArrayUtil.java @@ -0,0 +1,93 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.reflect.Array; + +public class ArrayUtil { + + private static final int INDEX_NOT_FOUND = -1; + + public static boolean isEmpty(Object array) { + if(array == null) { + return true; + } + + // not an array + if(!array.getClass().isArray()) { + return false; + } + + // check array length + return Array.getLength(array) == 0; + } + + public static boolean isNotEmpty(Object array) { + return !isEmpty(array); + } + + public static int indexOf(Object[] array, Object objectToFind) { + return indexOf(array, objectToFind, 0); + } + + public static int indexOf(Object[] array, Object objectToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + + if (startIndex < 0) { + startIndex = 0; + } + + if (objectToFind == null) { + for (int i = startIndex; i < array.length; i++) { + if (array[i] == null) { + return i; + } + } + } else { + for (int i = startIndex; i < array.length; i++) { + if (objectToFind.equals(array[i])) { + return i; + } + } + } + + return INDEX_NOT_FOUND; + } + + public static T[] addAll(T[] array1, T[] array2) { + if (array1 == null) { + return (T[]) clone(array2); + } else if (array2 == null) { + return (T[]) clone(array1); + } + @SuppressWarnings("unchecked") + T[] joinedArray = (T[]) Array.newInstance(array1.getClass().getComponentType(), array1.length + array2.length); + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + try { + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + } catch (ArrayStoreException ase) { + // Check if problem was due to incompatible types + /* + * We do this here, rather than before the copy because: - it would be a wasted check most of the time - + * safer, in case check turns out to be too strict + */ + final Class type1 = array1.getClass().getComponentType(); + final Class type2 = array2.getClass().getComponentType(); + if (!type1.isAssignableFrom(type2)) { + throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of " + + type1.getName()); + } + throw ase; // No, so rethrow original + } + return joinedArray; + } + + public static T[] clone(T[] array) { + if (array == null) { + return null; + } + + return (T[]) array.clone(); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/BeanUtil.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/BeanUtil.java new file mode 100644 index 000000000..b76246a02 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/BeanUtil.java @@ -0,0 +1,107 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.beans.Introspector; +import java.lang.reflect.Method; + +public abstract class BeanUtil { + + public static final String METHOD_GET_PREFIX = "get"; + public static final String METHOD_IS_PREFIX = "is"; + public static final String METHOD_SET_PREFIX = "set"; + + public static String getBeanGetterName(Method method) { + if (method == null) { + return null; + } + + int prefixLength = getBeanGetterPrefixLength(method); + if (prefixLength == 0) { + return null; + } + + String methodName = method.getName().substring(prefixLength); + return Introspector.decapitalize(methodName); + } + + private static int getBeanGetterPrefixLength(Method method) { + if (isObjectMethod(method)) { + return 0; + } + String methodName = method.getName(); + Class returnType = method.getReturnType(); + Class[] paramTypes = method.getParameterTypes(); + if (methodName.startsWith(METHOD_GET_PREFIX) && ((returnType != null) && (paramTypes.length == 0))) { + return 3; + } + + if (methodName.startsWith(METHOD_IS_PREFIX) && ((returnType != null) && (paramTypes.length == 0))) { + return 2; + } + + return 0; + } + + public static boolean isBeanProperty(Method method) { + if (method == null || isObjectMethod(method)) { + return false; + } + String methodName = method.getName(); + Class returnType = method.getReturnType(); + Class[] paramTypes = method.getParameterTypes(); + if (methodName.startsWith(METHOD_GET_PREFIX) && ((returnType != null) && (paramTypes.length == 0))) { + return true; + } + if (methodName.startsWith(METHOD_IS_PREFIX) && ((returnType != null) && (paramTypes.length == 0))) { + return true; + } + if (methodName.startsWith(METHOD_SET_PREFIX) && paramTypes.length == 1) { + return true; + } + + return false; + } + + public static boolean isBeanSetter(Method method) { + return getBeanSetterPrefixLength(method) != 0; + } + + private static int getBeanSetterPrefixLength(Method method) { + if (isObjectMethod(method)) { + return 0; + } + String methodName = method.getName(); + Class[] paramTypes = method.getParameterTypes(); + if (methodName.startsWith(METHOD_SET_PREFIX)) { + if (paramTypes.length == 1) { + return 3; + } + } + return 0; + } + + public static String getBeanSetterName(Method method) { + if (method == null) { + return null; + } + + int prefixLength = getBeanSetterPrefixLength(method); + if (prefixLength == 0) { + return null; + } + + String methodName = method.getName().substring(prefixLength); + return Introspector.decapitalize(methodName); + } + + public static boolean isBeanGetter(Method method) { + if (method == null) { + return false; + } + + return getBeanGetterPrefixLength(method) != 0; + } + + private static boolean isObjectMethod(Method method) { + return method.getDeclaringClass() == Object.class; + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassDescriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassDescriptor.java new file mode 100644 index 000000000..252debdf6 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassDescriptor.java @@ -0,0 +1,273 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class ClassDescriptor { + + protected final Class type; + protected final boolean scanAccessible; + protected final boolean scanStatics; + protected final boolean extendedProperties; + protected final boolean includeFieldsAsProperties; + protected final String propertyFieldPrefix; + protected final Class[] interfaces; + protected final Class[] superclasses; + protected int usageCount; + + private final boolean isArray; + private final boolean isMap; + private final boolean isList; + private final boolean isSet; + private final boolean isCollection; + private final Fields fields; + private final Methods methods; + private final Properties properties; + private final Constructors constructors; + + private final Annotations annotations; + + public ClassDescriptor(Class type, boolean scanAccessible, boolean extendedProperties, + boolean includeFieldsAsProperties, boolean scanStatics, String propertyFieldPrefix) { + this.type = type; + this.scanAccessible = scanAccessible; + this.extendedProperties = extendedProperties; + this.includeFieldsAsProperties = includeFieldsAsProperties; + this.propertyFieldPrefix = propertyFieldPrefix; + this.scanStatics = scanStatics; + + isArray = type.isArray(); + isMap = Map.class.isAssignableFrom(type); + isList = List.class.isAssignableFrom(type); + isSet = Set.class.isAssignableFrom(type); + isCollection = Collection.class.isAssignableFrom(type); + + interfaces = ClassUtil.getAllInterfacesAsArray(type); + superclasses = ClassUtil.getAllSuperclassesAsArray(type); + + fields = new Fields(this); + methods = new Methods(this); + properties = new Properties(this); + constructors = new Constructors(this); + + annotations = new Annotations(type); + } + + public Class getType() { + return type; + } + + public boolean isScanAccessible() { + return scanAccessible; + } + + public boolean isExtendedProperties() { + return extendedProperties; + } + + public boolean isIncludeFieldsAsProperties() { + return includeFieldsAsProperties; + } + + public String getPropertyFieldPrefix() { + return propertyFieldPrefix; + } + + protected void increaseUsageCount() { + usageCount++; + } + + public int getUsageCount() { + return usageCount; + } + + public boolean isArray() { + return isArray; + } + + public boolean isMap() { + return isMap; + } + + public boolean isList() { + return isList; + } + + public boolean isSet() { + return isSet; + } + + public boolean isCollection() { + return isCollection; + } + + protected Fields getFields() { + return fields; + } + + public FieldDescriptor getFieldDescriptor(String name, boolean declared) { + FieldDescriptor fieldDescriptor = getFields().getFieldDescriptor(name); + + if (fieldDescriptor != null) { + if (!fieldDescriptor.matchDeclared(declared)) { + return null; + } + } + + return fieldDescriptor; + } + + public FieldDescriptor[] getAllFieldDescriptors() { + return getFields().getAllFieldDescriptors(); + } + + protected Methods getMethods() { + return methods; + } + + public MethodDescriptor getMethodDescriptor(String name, boolean declared) { + MethodDescriptor methodDescriptor = getMethods().getMethodDescriptor(name); + + if ((methodDescriptor != null) && methodDescriptor.matchDeclared(declared)) { + return methodDescriptor; + } + + return methodDescriptor; + } + + public MethodDescriptor getMethodDescriptor(String name, Class[] params, boolean declared) { + MethodDescriptor methodDescriptor = getMethods().getMethodDescriptor(name, params); + + if ((methodDescriptor != null) && methodDescriptor.matchDeclared(declared)) { + return methodDescriptor; + } + + return null; + } + + public MethodDescriptor[] getAllMethodDescriptors(String name) { + return getMethods().getAllMethodDescriptors(name); + } + + public MethodDescriptor[] getAllMethodDescriptors() { + return getMethods().getAllMethodDescriptors(); + } + + // ---------------------------------------------------------------- + // properties + + protected Properties getProperties() { + return properties; + } + + public PropertyDescriptor getPropertyDescriptor(String name, boolean declared) { + PropertyDescriptor propertyDescriptor = getProperties().getPropertyDescriptor(name); + + if ((propertyDescriptor != null) && propertyDescriptor.matchDeclared(declared)) { + return propertyDescriptor; + } + + return null; + } + + public PropertyDescriptor[] getAllPropertyDescriptors() { + return getProperties().getAllPropertyDescriptors(); + } + + // ---------------------------------------------------------------- + // constructors + + protected Constructors getConstructors() { + return constructors; + } + + public ConstructorDescriptor getDefaultCtorDescriptor(boolean declared) { + ConstructorDescriptor defaultConstructor = getConstructors().getDefaultCtor(); + + if ((defaultConstructor != null) && defaultConstructor.matchDeclared(declared)) { + return defaultConstructor; + } + return null; + } + + public ConstructorDescriptor getConstructorDescriptor(Class[] args, boolean declared) { + ConstructorDescriptor constructorDescriptor = getConstructors().getCtorDescriptor(args); + + if ((constructorDescriptor != null) && constructorDescriptor.matchDeclared(declared)) { + return constructorDescriptor; + } + return null; + } + + public ConstructorDescriptor[] getAllConstructorDescriptors() { + return getConstructors().getAllCtorDescriptors(); + } + + // ---------------------------------------------------------------- + // annotations + + protected Annotations getAnnotations() { + return annotations; + } + + public AnnotationDescriptor getAnnotationDescriptor(Class clazz) { + return annotations.getAnnotationDescriptor(clazz); + } + + public AnnotationDescriptor[] getAllAnnotationDescriptors() { + return annotations.getAllAnnotationDescriptors(); + } + + public Class[] getAllInterfaces() { + return interfaces; + } + + public Class[] getAllSuperclasses() { + return superclasses; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ClassDescriptor [type=") + .append(type) + .append(", scanAccessible=") + .append(scanAccessible) + .append(", extendedProperties=") + .append(extendedProperties) + .append(", includeFieldsAsProperties=") + .append(includeFieldsAsProperties) + .append(", propertyFieldPrefix=") + .append(propertyFieldPrefix) + .append(", usageCount=") + .append(usageCount) + .append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ClassDescriptor other = (ClassDescriptor) obj; + return Objects.equals(type, other.type); + } + + + public boolean isScanStatics() { + return scanStatics; + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassIntrospector.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassIntrospector.java new file mode 100644 index 000000000..c832b6293 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassIntrospector.java @@ -0,0 +1,107 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class ClassIntrospector { + + protected final Map, ClassDescriptor> cache = new LinkedHashMap<>(); + protected final boolean scanAccessible; + protected final boolean enhancedProperties; + protected final boolean includeFieldsAsProperties; + protected final boolean scanStatics; + protected final String propertyFieldPrefix; + + private ClassIntrospector(Builder builder) { + this.scanAccessible = builder.scanAccessible; + this.enhancedProperties = builder.enhancedProperties; + this.includeFieldsAsProperties = builder.includeFieldsAsProperties; + this.propertyFieldPrefix = builder.propertyFieldPrefix; + this.scanStatics = builder.scanStatics ; + } + + public ClassIntrospector() { + this(true, true, true, true, null); + } + + public ClassIntrospector(boolean scanAccessible, + boolean enhancedProperties, + boolean includeFieldsAsProperties, + boolean scanStatics, + String propertyFieldPrefix) { + this.scanAccessible = scanAccessible; + this.enhancedProperties = enhancedProperties; + this.includeFieldsAsProperties = includeFieldsAsProperties; + this.propertyFieldPrefix = propertyFieldPrefix; + this.scanStatics = scanStatics; + } + + public ClassDescriptor introspect(Class type) { + ClassDescriptor cd = cache.computeIfAbsent(type, this::getClassDescriptor); + + cd.increaseUsageCount(); + + return cd; + } + + private ClassDescriptor getClassDescriptor(Class type) { + return new ClassDescriptor(type, + scanAccessible, + enhancedProperties, + includeFieldsAsProperties, + scanStatics, + propertyFieldPrefix); + } + + /** + * Creates builder to build {@link ClassIntrospector}. + * @return created builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder to build {@link ClassIntrospector}. + */ + public static final class Builder { + + public boolean scanStatics = false; + private boolean scanAccessible = true; + private boolean enhancedProperties = true; + private boolean includeFieldsAsProperties = true; + private String propertyFieldPrefix = null; + + private Builder() { + } + + public Builder withScanAccessible(boolean scanAccessible) { + this.scanAccessible = scanAccessible; + return this; + } + + public Builder withEnhancedProperties(boolean enhancedProperties) { + this.enhancedProperties = enhancedProperties; + return this; + } + + public Builder withIncludeFieldsAsProperties(boolean includeFieldsAsProperties) { + this.includeFieldsAsProperties = includeFieldsAsProperties; + return this; + } + + public Builder withPropertyFieldPrefix(String propertyFieldPrefix) { + this.propertyFieldPrefix = propertyFieldPrefix; + return this; + } + + public Builder withScanStatics(boolean includeStatics) { + this.scanStatics = includeStatics; + return this; + } + + public ClassIntrospector build() { + return new ClassIntrospector(this); + } + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassUtil.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassUtil.java new file mode 100644 index 000000000..6ce510694 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ClassUtil.java @@ -0,0 +1,70 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.util.ArrayList; +import java.util.List; + +public class ClassUtil { + + public static Class[] getAllInterfacesAsArray(Class clazz) { + if (clazz == null) { + return null; + } + + List> interfacesFound = new ArrayList<>(); + getAllInterfaces(clazz, interfacesFound); + + return interfacesFound.toArray(new Class[0]); + } + + private static void getAllInterfaces(Class clazz, List> interfacesFound) { + while (clazz != null) { + Class[] interfaces = clazz.getInterfaces(); + + for (int i = 0; i < interfaces.length; i++) { + if (!interfacesFound.contains(interfaces[i])) { + interfacesFound.add(interfaces[i]); + getAllInterfaces(interfaces[i], interfacesFound); + } + } + + clazz = clazz.getSuperclass(); + } + } + + public static List> getAllInterfaces(Class clazz) { + if (clazz == null) { + return null; + } + + List> interfacesFound = new ArrayList<>(); + getAllInterfaces(clazz, interfacesFound); + + return interfacesFound; + } + public static List> getAllSuperclasses(Class clazz) { + if (clazz == null) { + return null; + } + List> classes = new ArrayList<>(); + Class superclass = clazz.getSuperclass(); + while (superclass != null && superclass != Object.class) { + classes.add(superclass); + superclass = superclass.getSuperclass(); + } + return classes; + } + + public static Class[] getAllSuperclassesAsArray(Class clazz) { + if (clazz == null) { + return null; + } + List> classes = new ArrayList<>(); + Class superclass = clazz.getSuperclass(); + while (superclass != null && superclass != Object.class) { + classes.add(superclass); + superclass = superclass.getSuperclass(); + } + return classes.toArray(new Class[0]); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ConstructorDescriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ConstructorDescriptor.java new file mode 100644 index 000000000..6481396b0 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ConstructorDescriptor.java @@ -0,0 +1,77 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Objects; + +public class ConstructorDescriptor extends Descriptor { + + protected final Constructor constructor; + protected final Class[] parameters; + + public ConstructorDescriptor(ClassDescriptor classDescriptor, Constructor constructor) { + super(classDescriptor, ReflectionUtil.isPublic(constructor)); + this.constructor = constructor; + this.parameters = constructor.getParameterTypes(); + + annotations = new Annotations(constructor); + + ReflectionUtil.forceAccess(constructor); + } + + @Override + public String getName() { + return constructor.getName(); + } + + public Class getDeclaringClass() { + return constructor.getDeclaringClass(); + } + + public Constructor getConstructor() { + return constructor; + } + + public Class[] getParameters() { + return parameters; + } + + public boolean isDefault() { + return parameters.length == 0; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ConstructorDescriptor [constructor=") + .append(constructor) + .append(", parameters=") + .append(Arrays.toString(parameters)) + .append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Arrays.hashCode(parameters); + result = prime * result + Objects.hash(constructor); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + ConstructorDescriptor other = (ConstructorDescriptor) obj; + return Objects.equals(constructor, other.constructor) + && Arrays.equals(parameters, other.parameters); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Constructors.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Constructors.java new file mode 100644 index 000000000..7bc5f558a --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Constructors.java @@ -0,0 +1,106 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Objects; + +public class Constructors { + + protected final ClassDescriptor classDescriptor; + protected final ConstructorDescriptor[] allConstructors; + protected ConstructorDescriptor defaultConstructor; + + public Constructors(ClassDescriptor classDescriptor) { + this.classDescriptor = classDescriptor; + this.allConstructors = inspectConstructors(); + } + + protected ConstructorDescriptor[] inspectConstructors() { + Class type = classDescriptor.getType(); + Constructor[] ctors = type.getDeclaredConstructors(); + + ConstructorDescriptor[] allConstructors = new ConstructorDescriptor[ctors.length]; + + for (int i = 0; i < ctors.length; i++) { + Constructor ctor = ctors[i]; + + ConstructorDescriptor ctorDescriptor = createCtorDescriptor(ctor); + allConstructors[i] = ctorDescriptor; + + if (ctorDescriptor.isDefault()) { + defaultConstructor = ctorDescriptor; + } + } + + return allConstructors; + } + + protected ConstructorDescriptor createCtorDescriptor(Constructor constructor) { + return new ConstructorDescriptor(classDescriptor, constructor); + } + + public ConstructorDescriptor getDefaultCtor() { + return defaultConstructor; + } + + public ConstructorDescriptor getCtorDescriptor(Class...args) { + ctors: for (ConstructorDescriptor ctorDescriptor : allConstructors) { + Class[] arg = ctorDescriptor.getParameters(); + + if (arg.length != args.length) { + continue; + } + + for (int j = 0; j < arg.length; j++) { + if (arg[j] != args[j]) { + continue ctors; + } + } + + return ctorDescriptor; + } + return null; + } + + ConstructorDescriptor[] getAllCtorDescriptors() { + return allConstructors; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Constructors [classDescriptor=") + .append(classDescriptor) + .append(", allConstructors=") + .append(Arrays.toString(allConstructors)) + .append(", defaultConstructor=") + .append(defaultConstructor) + .append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(allConstructors); + result = prime * result + Objects.hash(classDescriptor, defaultConstructor); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Constructors other = (Constructors) obj; + return Arrays.equals(allConstructors, other.allConstructors) + && Objects.equals(classDescriptor, other.classDescriptor) + && Objects.equals(defaultConstructor, other.defaultConstructor); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Descriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Descriptor.java new file mode 100644 index 000000000..f1a050858 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Descriptor.java @@ -0,0 +1,77 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.annotation.Annotation; +import java.util.Objects; + +public abstract class Descriptor { + + protected final ClassDescriptor classDescriptor; + protected final boolean isPublic; + + protected Annotations annotations; + + protected Descriptor(ClassDescriptor classDescriptor, boolean isPublic) { + this.classDescriptor = classDescriptor; + this.isPublic = isPublic; + } + + public ClassDescriptor getClassDescriptor() { + return classDescriptor; + } + + public boolean isPublic() { + return isPublic; + } + + public boolean matchDeclared(boolean declared) { + if (!declared) { + return isPublic; + } + + return true; + } + + protected Annotations getAnnotations() { + return annotations; + } + + public AnnotationDescriptor getAnnotationDescriptor(Class clazz) { + return annotations.getAnnotationDescriptor(clazz); + } + + public A getAnnotation(Class clazz) { + AnnotationDescriptor annotationDescriptor = annotations.getAnnotationDescriptor(clazz); + if (annotationDescriptor == null) { + return null; + } + + return annotationDescriptor.getAnnotation(); + } + + public AnnotationDescriptor[] getAllAnnotationDescriptors() { + return annotations.getAllAnnotationDescriptors(); + } + + public abstract String getName(); + + @Override + public int hashCode() { + return Objects.hash(annotations, classDescriptor, isPublic); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Descriptor other = (Descriptor) obj; + return Objects.equals(annotations, other.annotations) + && Objects.equals(classDescriptor, other.classDescriptor) + && isPublic == other.isPublic; + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/FieldDescriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/FieldDescriptor.java new file mode 100644 index 000000000..9a5504d6b --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/FieldDescriptor.java @@ -0,0 +1,126 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import java.util.Objects; + +public class FieldDescriptor extends Descriptor implements Getter, Setter { + + protected final Field field; + protected final Type type; + protected final Class rawType; + protected final Class rawComponentType; + protected final Class rawKeyComponentType; + + public FieldDescriptor(ClassDescriptor classDescriptor, Field field) { + super(classDescriptor, ReflectionUtil.isPublic(field)); + this.field = field; + this.type = field.getGenericType(); + this.rawType = ReflectionUtil.getRawType(type, classDescriptor.getType()); + + Class[] componentTypes = ReflectionUtil.getComponentTypes(type, classDescriptor.getType()); + if (componentTypes != null) { + this.rawComponentType = componentTypes[componentTypes.length - 1]; + this.rawKeyComponentType = componentTypes[0]; + } else { + this.rawComponentType = null; + this.rawKeyComponentType = null; + } + + annotations = new Annotations(field); + + ReflectionUtil.forceAccess(field); + } + + @Override + public String getName() { + return field.getName(); + } + + public Class getDeclaringClass() { + return field.getDeclaringClass(); + } + + public Field getField() { + return field; + } + + public Class getRawType() { + return rawType; + } + + public Class getRawComponentType() { + return rawComponentType; + } + + public Class getRawKeyComponentType() { + return rawKeyComponentType; + } + + public Class[] resolveRawComponentTypes() { + return ReflectionUtil.getComponentTypes(type, classDescriptor.getType()); + } + + @Override + public Object invokeGetter(Object target) throws InvocationTargetException, IllegalAccessException { + return field.get(target); + } + + @Override + public Class getGetterRawType() { + return getRawType(); + } + + @Override + public Class getGetterRawComponentType() { + return getRawComponentType(); + } + + @Override + public Class getGetterRawKeyComponentType() { + return getRawKeyComponentType(); + } + + @Override + public void invokeSetter(Object target, Object argument) throws IllegalAccessException { + field.set(target, argument); + } + + @Override + public Class getSetterRawType() { + return getRawType(); + } + + @Override + public Class getSetterRawComponentType() { + return getRawComponentType(); + } + + @Override + public String toString() { + return classDescriptor.getType().getSimpleName() + '#' + field.getName(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(field, type); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + FieldDescriptor other = (FieldDescriptor) obj; + return Objects.equals(field, other.field) && Objects.equals(type, other.type); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Fields.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Fields.java new file mode 100644 index 000000000..d022667ed --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Fields.java @@ -0,0 +1,84 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; + +public class Fields { + + public static final String SERIAL_VERSION_UID = "serialVersionUID"; + + protected final ClassDescriptor classDescriptor; + protected final Map fieldsMap; + + // cache + private FieldDescriptor[] allFields; + + public Fields(ClassDescriptor classDescriptor) { + this.classDescriptor = classDescriptor; + this.fieldsMap = inspectFields(); + } + + protected Map inspectFields() { + boolean scanAccessible = classDescriptor.isScanAccessible(); + boolean scanStatics = classDescriptor.isScanStatics(); + Class type = classDescriptor.getType(); + + Field[] fields = + scanAccessible ? ReflectionUtil.getAccessibleFields(type) : ReflectionUtil.getAllFieldsOfClass(type); + + Map map = new LinkedHashMap<>(fields.length); + + for (Field field : fields) { + String fieldName = field.getName(); + + if (fieldName.equals(SERIAL_VERSION_UID)) { + continue; + } + + if (!scanStatics && Modifier.isStatic(field.getModifiers())) { + continue; + } + + map.put(fieldName, createFieldDescriptor(field)); + } + + return map; + } + + protected FieldDescriptor createFieldDescriptor(Field field) { + return new FieldDescriptor(classDescriptor, field); + } + + public FieldDescriptor getFieldDescriptor(String name) { + return fieldsMap.get(name); + } + + public FieldDescriptor[] getAllFieldDescriptors() { + if (allFields == null) { + FieldDescriptor[] allFields = new FieldDescriptor[fieldsMap.size()]; + + int index = 0; + for (FieldDescriptor fieldDescriptor : fieldsMap.values()) { + allFields[index] = fieldDescriptor; + index++; + } + + Arrays.sort(allFields, new Comparator() { + @Override + public int compare(FieldDescriptor fd1, FieldDescriptor fd2) { + return fd1.getField().getName().compareTo(fd2.getField().getName()); + } + }); + + this.allFields = allFields; + } + + return allFields; + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Getter.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Getter.java new file mode 100644 index 000000000..37da6e6f5 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Getter.java @@ -0,0 +1,13 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.reflect.InvocationTargetException; + +public interface Getter { + Object invokeGetter(Object target) throws InvocationTargetException, IllegalAccessException; + + Class getGetterRawType(); + + Class getGetterRawComponentType(); + + Class getGetterRawKeyComponentType(); +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/MethodDescriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/MethodDescriptor.java new file mode 100644 index 000000000..64a5dc515 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/MethodDescriptor.java @@ -0,0 +1,158 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Objects; + +public class MethodDescriptor extends Descriptor implements Getter, Setter { + + protected final Method method; + protected final Type returnType; + protected final Class rawReturnType; + protected final Class rawReturnComponentType; + protected final Class rawReturnKeyComponentType; + protected final Class[] rawParameterTypes; + protected final Class[] rawParameterComponentTypes; + + public MethodDescriptor(ClassDescriptor classDescriptor, Method method) { + super(classDescriptor, ReflectionUtil.isPublic(method)); + this.method = method; + this.returnType = method.getGenericReturnType(); + this.rawReturnType = ReflectionUtil.getRawType(returnType, classDescriptor.getType()); + + Class[] componentTypes = ReflectionUtil.getComponentTypes(returnType, classDescriptor.getType()); + if (componentTypes != null) { + this.rawReturnComponentType = componentTypes[componentTypes.length - 1]; + this.rawReturnKeyComponentType = componentTypes[0]; + } else { + this.rawReturnComponentType = null; + this.rawReturnKeyComponentType = null; + } + + annotations = new Annotations(method); + + ReflectionUtil.forceAccess(method); + + Type[] params = method.getGenericParameterTypes(); + Type[] genericParams = method.getGenericParameterTypes(); + + rawParameterTypes = new Class[params.length]; + rawParameterComponentTypes = genericParams.length == 0 ? null : new Class[params.length]; + + for (int i = 0; i < params.length; i++) { + Type type = params[i]; + rawParameterTypes[i] = ReflectionUtil.getRawType(type, classDescriptor.getType()); + if (rawParameterComponentTypes != null) { + rawParameterComponentTypes[i] = + ReflectionUtil.getComponentType(genericParams[i], classDescriptor.getType()); + } + } + } + + @Override + public String getName() { + return method.getName(); + } + + public Class getDeclaringClass() { + return method.getDeclaringClass(); + } + + public Method getMethod() { + return method; + } + + public Class getRawReturnType() { + return rawReturnType; + } + + public Class getRawReturnComponentType() { + return rawReturnComponentType; + } + + public Class getRawReturnKeyComponentType() { + return rawReturnKeyComponentType; + } + + public Class[] resolveRawReturnComponentTypes() { + return ReflectionUtil.getComponentTypes(returnType, classDescriptor.getType()); + } + + public Class[] getRawParameterTypes() { + return rawParameterTypes; + } + + public Class[] getRawParameterComponentTypes() { + return rawParameterComponentTypes; + } + + @Override + public Object invokeGetter(Object target) throws InvocationTargetException, IllegalAccessException { + return method.invoke(target); + } + + @Override + public Class getGetterRawType() { + return getRawReturnType(); + } + + @Override + public Class getGetterRawComponentType() { + return getRawReturnComponentType(); + } + + @Override + public Class getGetterRawKeyComponentType() { + return getRawReturnKeyComponentType(); + } + + @Override + public void invokeSetter(Object target, Object argument) throws IllegalAccessException, InvocationTargetException { + method.invoke(target, argument); + } + + @Override + public Class getSetterRawType() { + return getRawParameterTypes()[0]; + } + + @Override + public Class getSetterRawComponentType() { + Class[] ts = getRawParameterComponentTypes(); + if (ts == null) { + return null; + } + return ts[0]; + } + + @Override + public String toString() { + return classDescriptor.getType().getSimpleName() + '#' + method.getName() + "()"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Arrays.hashCode(rawParameterTypes); + result = prime * result + Objects.hash(method, returnType); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + MethodDescriptor other = (MethodDescriptor) obj; + return Objects.equals(method, other.method) && Arrays.equals(rawParameterTypes, + other.rawParameterTypes) && Objects.equals(returnType, + other.returnType); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Methods.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Methods.java new file mode 100644 index 000000000..820a560e9 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Methods.java @@ -0,0 +1,116 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Methods { + + protected final ClassDescriptor classDescriptor; + protected final Map methodsMap; + + // cached + private MethodDescriptor[] allMethods; + + public Methods(ClassDescriptor classDescriptor) { + this.classDescriptor = classDescriptor; + this.methodsMap = inspectMethods(); + } + + protected Map inspectMethods() { + boolean scanAccessible = classDescriptor.isScanAccessible(); + boolean scanStatics = classDescriptor.isScanStatics(); + Class type = classDescriptor.getType(); + + Method[] methods = + scanAccessible ? ReflectionUtil.getAccessibleMethods(type) : ReflectionUtil.getAllMethodsOfClass(type); + + Map map = new LinkedHashMap<>(methods.length); + + for (Method method : methods) { + + if(!scanStatics && Modifier.isStatic(method.getModifiers())) { + continue; + } + + String methodName = method.getName(); + + MethodDescriptor[] mds = map.get(methodName); + + if (mds == null) { + mds = new MethodDescriptor[1]; + } else { + mds = Arrays.copyOf(mds, mds.length + 1); + } + + map.put(methodName, mds); + + mds[mds.length - 1] = createMethodDescriptor(method); + } + + return map; + } + + protected MethodDescriptor createMethodDescriptor(Method method) { + return new MethodDescriptor(classDescriptor, method); + } + + public MethodDescriptor getMethodDescriptor(String name, Class[] paramTypes) { + MethodDescriptor[] methodDescriptors = methodsMap.get(name); + if (methodDescriptors == null) { + return null; + } + for (int i = 0; i < methodDescriptors.length; i++) { + Method method = methodDescriptors[i].getMethod(); + if (ObjectUtil.isEquals(method.getParameterTypes(), paramTypes)) { + return methodDescriptors[i]; + } + } + return null; + } + + public MethodDescriptor getMethodDescriptor(String name) { + MethodDescriptor[] methodDescriptors = methodsMap.get(name); + if (methodDescriptors == null) { + return null; + } + if (methodDescriptors.length != 1) { + throw new IllegalArgumentException("Method name not unique: " + name); + } + return methodDescriptors[0]; + } + + public MethodDescriptor[] getAllMethodDescriptors(String name) { + return methodsMap.get(name); + } + + public MethodDescriptor[] getAllMethodDescriptors() { + if (allMethods == null) { + List allMethodsList = new ArrayList<>(); + + for (MethodDescriptor[] methodDescriptors : methodsMap.values()) { + Collections.addAll(allMethodsList, methodDescriptors); + } + + MethodDescriptor[] allMethods = allMethodsList.toArray(new MethodDescriptor[allMethodsList.size()]); + + Arrays.sort(allMethods, new Comparator() { + @Override + public int compare(MethodDescriptor md1, MethodDescriptor md2) { + return md1.getMethod().getName().compareTo(md2.getMethod().getName()); + } + }); + + this.allMethods = allMethods; + } + return allMethods; + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ObjectUtil.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ObjectUtil.java new file mode 100644 index 000000000..615109381 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ObjectUtil.java @@ -0,0 +1,60 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; + +public class ObjectUtil { + + public static boolean isAnyNull(Object... objects) { + if(objects == null) { + return true; + } + + return Stream.of(objects).anyMatch(Objects::isNull); + } + + public static boolean isEquals(Object object1, Object object2) { + if (object1 == object2) { + return true; + } + + if (object1 == null || object2 == null) { + return false; + } + + if (!object1.getClass().equals(object2.getClass())) { + return false; + } + + if (object1 instanceof Object[]) { + return Arrays.deepEquals((Object[]) object1, (Object[]) object2); + } + if (object1 instanceof int[]) { + return Arrays.equals((int[]) object1, (int[]) object2); + } + if (object1 instanceof long[]) { + return Arrays.equals((long[]) object1, (long[]) object2); + } + if (object1 instanceof short[]) { + return Arrays.equals((short[]) object1, (short[]) object2); + } + if (object1 instanceof byte[]) { + return Arrays.equals((byte[]) object1, (byte[]) object2); + } + if (object1 instanceof double[]) { + return Arrays.equals((double[]) object1, (double[]) object2); + } + if (object1 instanceof float[]) { + return Arrays.equals((float[]) object1, (float[]) object2); + } + if (object1 instanceof char[]) { + return Arrays.equals((char[]) object1, (char[]) object2); + } + if (object1 instanceof boolean[]) { + return Arrays.equals((boolean[]) object1, (boolean[]) object2); + } + return object1.equals(object2); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Properties.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Properties.java new file mode 100644 index 000000000..9a827f810 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Properties.java @@ -0,0 +1,175 @@ +package com.introproventures.graphql.jpa.query.introspection; + + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; + +public class Properties { + + protected final ClassDescriptor classDescriptor; + protected final Map propertyDescriptors; + + // cache + private PropertyDescriptor[] allProperties; + + public Properties(ClassDescriptor classDescriptor) { + this.classDescriptor = classDescriptor; + this.propertyDescriptors = inspectProperties(); + } + + protected Map inspectProperties() { + boolean scanAccessible = classDescriptor.isScanAccessible(); + Class type = classDescriptor.getType(); + + Map map = new LinkedHashMap<>(); + + Method[] methods = + scanAccessible ? ReflectionUtil.getAccessibleMethods(type) : ReflectionUtil.getAllMethodsOfClass(type); + + for (int iteration = 0; iteration < 2; iteration++) { + // first find the getters, and then the setters! + for (Method method : methods) { + if (Modifier.isStatic(method.getModifiers())) { + continue; // ignore static methods + } + + boolean add = false; + boolean issetter = false; + + String propertyName; + + if (iteration == 0) { + propertyName = BeanUtil.getBeanGetterName(method); + if (propertyName != null) { + add = true; + issetter = false; + } + } else { + propertyName = BeanUtil.getBeanSetterName(method); + if (propertyName != null) { + add = true; + issetter = true; + } + } + + if (add == true) { + MethodDescriptor methodDescriptor = + classDescriptor.getMethodDescriptor(method.getName(), method.getParameterTypes(), true); + addProperty(map, propertyName, methodDescriptor, issetter); + } + } + } + + if (classDescriptor.isIncludeFieldsAsProperties()) { + FieldDescriptor[] fieldDescriptors = classDescriptor.getAllFieldDescriptors(); + String prefix = classDescriptor.getPropertyFieldPrefix(); + + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + String name = fieldDescriptor.getField().getName(); + + if (prefix != null) { + if (!name.startsWith(prefix)) { + continue; + } + name = name.substring(prefix.length()); + } + + if (!map.containsKey(name)) { + // add missing field as a potential property + map.put(name, createPropertyDescriptor(name, fieldDescriptor)); + } + } + + } + + return map; + } + + protected void addProperty(Map map, String name, MethodDescriptor methodDescriptor, + boolean isSetter) { + MethodDescriptor setterMethod = isSetter ? methodDescriptor : null; + MethodDescriptor getterMethod = isSetter ? null : methodDescriptor; + + PropertyDescriptor existing = map.get(name); + + if (existing == null) { + // new property, just add it + PropertyDescriptor propertyDescriptor = createPropertyDescriptor(name, getterMethod, setterMethod); + + map.put(name, propertyDescriptor); + return; + } + + if (!isSetter) { + // use existing setter + setterMethod = existing.getWriteMethodDescriptor(); + // check existing + MethodDescriptor existingMethodDescriptor = existing.getReadMethodDescriptor(); + if (existingMethodDescriptor != null) { + // check for special case of double get/is + + // getter with the same name already exist + String methodName = methodDescriptor.getMethod().getName(); + String existingMethodName = existingMethodDescriptor.getMethod().getName(); + + if (existingMethodName.startsWith(BeanUtil.METHOD_IS_PREFIX) + && methodName.startsWith(BeanUtil.METHOD_GET_PREFIX)) { + return; + } + } + } else { + // setter + // use existing getter + getterMethod = existing.getReadMethodDescriptor(); + + if (getterMethod.getMethod().getReturnType() != setterMethod.getMethod().getParameterTypes()[0]) { + return; + } + } + + PropertyDescriptor propertyDescriptor = createPropertyDescriptor(name, getterMethod, setterMethod); + + map.put(name, propertyDescriptor); + } + + protected PropertyDescriptor createPropertyDescriptor(String name, MethodDescriptor getterMethod, + MethodDescriptor setterMethod) { + return new PropertyDescriptor(classDescriptor, name, getterMethod, setterMethod); + } + + protected PropertyDescriptor createPropertyDescriptor(String name, FieldDescriptor fieldDescriptor) { + return new PropertyDescriptor(classDescriptor, name, fieldDescriptor); + } + + public PropertyDescriptor getPropertyDescriptor(String name) { + return propertyDescriptors.get(name); + } + + public PropertyDescriptor[] getAllPropertyDescriptors() { + if (allProperties == null) { + PropertyDescriptor[] allProperties = new PropertyDescriptor[propertyDescriptors.size()]; + + int index = 0; + for (PropertyDescriptor propertyDescriptor : propertyDescriptors.values()) { + allProperties[index] = propertyDescriptor; + index++; + } + + Arrays.sort(allProperties, new Comparator() { + @Override + public int compare(PropertyDescriptor pd1, PropertyDescriptor pd2) { + return pd1.getName().compareTo(pd2.getName()); + } + }); + + this.allProperties = allProperties; + } + + return allProperties; + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/PropertyDescriptor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/PropertyDescriptor.java new file mode 100644 index 000000000..c8c380ec8 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/PropertyDescriptor.java @@ -0,0 +1,237 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +public class PropertyDescriptor extends Descriptor { + + protected final String name; + protected final MethodDescriptor readMethodDescriptor; + protected final MethodDescriptor writeMethodDescriptor; + protected final FieldDescriptor fieldDescriptor; + + protected Class type; + protected Getter[] getters; + protected Setter[] setters; + + public PropertyDescriptor(ClassDescriptor classDescriptor, String propertyName, FieldDescriptor fieldDescriptor) { + super(classDescriptor, false); + this.name = propertyName; + this.readMethodDescriptor = null; + this.writeMethodDescriptor = null; + this.fieldDescriptor = fieldDescriptor; + this.annotations = new Annotations(fieldDescriptor.getField()); + } + + public PropertyDescriptor(ClassDescriptor classDescriptor, String propertyName, MethodDescriptor readMethod, + MethodDescriptor writeMethod) { + super(classDescriptor, ((readMethod == null) || readMethod.isPublic()) + & (writeMethod == null || writeMethod.isPublic())); + this.name = propertyName; + this.readMethodDescriptor = readMethod; + this.writeMethodDescriptor = writeMethod; + + if (classDescriptor.isExtendedProperties()) { + this.fieldDescriptor = findField(propertyName); + if(fieldDescriptor != null) { + this.annotations = new Annotations(fieldDescriptor.getField()); + } + } else { + this.fieldDescriptor = null; + if(readMethod != null) { + this.annotations = new Annotations(readMethod.getMethod()); + } + else if(writeMethod != null) { + this.annotations = new Annotations(writeMethod.getMethod()); + } + } + + + } + + protected FieldDescriptor findField(String fieldName) { + String prefix = classDescriptor.getPropertyFieldPrefix(); + + if (prefix != null) { + fieldName = prefix + fieldName; + } + + return classDescriptor.getFieldDescriptor(fieldName, true); + } + + @Override + public String getName() { + return name; + } + + public MethodDescriptor getReadMethodDescriptor() { + return readMethodDescriptor; + } + + public MethodDescriptor getWriteMethodDescriptor() { + return writeMethodDescriptor; + } + + public FieldDescriptor getFieldDescriptor() { + return fieldDescriptor; + } + + public boolean isFieldOnlyDescriptor() { + return (readMethodDescriptor == null) && (writeMethodDescriptor == null); + } + + public Class getType() { + if (type == null) { + if (readMethodDescriptor != null) { + type = readMethodDescriptor.getMethod().getReturnType(); + } else if (writeMethodDescriptor != null) { + type = writeMethodDescriptor.getMethod().getParameterTypes()[0]; + } else if (fieldDescriptor != null) { + type = fieldDescriptor.getField().getType(); + } + } + + return type; + } + + public Getter getGetter(boolean declared) { + if (getters == null) { + getters = new Getter[] { createGetter(false), createGetter(true), }; + } + + return getters[declared ? 1 : 0]; + } + + protected Getter createGetter(boolean declared) { + if (readMethodDescriptor != null) { + if (readMethodDescriptor.matchDeclared(declared)) { + return readMethodDescriptor; + } + } + if (fieldDescriptor != null) { + if (fieldDescriptor.matchDeclared(declared)) { + return fieldDescriptor; + } + } + + return null; + } + + public Setter getSetter(boolean declared) { + if (setters == null) { + setters = new Setter[] { createSetter(false), createSetter(true), }; + } + + return setters[declared ? 1 : 0]; + } + + protected Setter createSetter(boolean declared) { + if (writeMethodDescriptor != null) { + if (writeMethodDescriptor.matchDeclared(declared)) { + return writeMethodDescriptor; + } + } + if (fieldDescriptor != null) { + if (fieldDescriptor.matchDeclared(declared)) { + return fieldDescriptor; + } + } + + return null; + } + + public Class resolveKeyType(boolean declared) { + Class keyType = null; + + Getter getter = getGetter(declared); + + if (getter != null) { + keyType = getter.getGetterRawKeyComponentType(); + } + + if (keyType == null) { + FieldDescriptor fieldDescriptor = getFieldDescriptor(); + + if (fieldDescriptor != null) { + keyType = fieldDescriptor.getRawKeyComponentType(); + } + } + + return keyType; + } + + public Class resolveComponentType(boolean declared) { + Class componentType = null; + + Getter getter = getGetter(declared); + + if (getter != null) { + componentType = getter.getGetterRawComponentType(); + } + + if (componentType == null) { + FieldDescriptor fieldDescriptor = getFieldDescriptor(); + + if (fieldDescriptor != null) { + componentType = fieldDescriptor.getRawComponentType(); + } + } + + return componentType; + } + + // add + public Field getField() { + Class clazz = this.getClassDescriptor().getType(); + + return ReflectionUtil.getField(clazz, this.getName()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PropertyDescriptor [name=") + .append(name) + .append(", readMethodDescriptor=") + .append(readMethodDescriptor) + .append(", writeMethodDescriptor=") + .append(writeMethodDescriptor) + .append(", fieldDescriptor=") + .append(fieldDescriptor) + .append(", type=") + .append(type) + .append(", getters=") + .append(Arrays.toString(getters)) + .append(", setters=") + .append(Arrays.toString(setters)) + .append(", classDescriptor=") + .append(classDescriptor) + .append(", isPublic=") + .append(isPublic) + .append(", annotations=") + .append(annotations) + .append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(name, type); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + PropertyDescriptor other = (PropertyDescriptor) obj; + return Objects.equals(name, other.name) && Objects.equals(type, other.type); + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ReflectionUtil.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ReflectionUtil.java new file mode 100644 index 000000000..70aa1f361 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/ReflectionUtil.java @@ -0,0 +1,635 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public abstract class ReflectionUtil { + + private static final Logger logger = LoggerFactory.getLogger(ReflectionUtil.class); + + public static Field[] getAllFieldsOfClass(Class clazz) { + if (clazz == null) { + return null; + } + + return getAllFieldsOfClass0(clazz); + } + + public static Method[] getAccessibleMethods(Class clazz) { + return getAccessibleMethods(clazz, Object.class); + } + + public static Method[] getAllMethodsOfClass(final Class clazz) { + if (clazz == null) { + return null; + } + Method[] methods = null; + Class itr = clazz; + while (itr != null && !itr.equals(Object.class)) { + methods = ArrayUtil.addAll(itr.getDeclaredMethods(), methods); + itr = itr.getSuperclass(); + } + return methods; + } + + public static Method[] getAccessibleMethods(Class clazz, Class limit) { + Package topPackage = clazz.getPackage(); + List methodList = new ArrayList<>(); + int topPackageHash = (topPackage == null) ? 0 : topPackage.hashCode(); + boolean top = true; + do { + if (clazz == null) { + break; + } + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (Modifier.isVolatile(method.getModifiers())) { + continue; + } + if (top) { + methodList.add(method); + continue; + } + int modifier = method.getModifiers(); + if (Modifier.isPrivate(modifier) || Modifier.isAbstract(modifier)) { + continue; + } + + if (Modifier.isPublic(modifier) || Modifier.isProtected(modifier)) { + addMethodIfNotExist(methodList, method); + continue; + } + // add super default methods from the same package + Package pckg = method.getDeclaringClass().getPackage(); + int pckgHash = (pckg == null) ? 0 : pckg.hashCode(); + if (pckgHash == topPackageHash) { + addMethodIfNotExist(methodList, method); + } + } + top = false; + } while ((clazz = clazz.getSuperclass()) != limit); + + Method[] methods = new Method[methodList.size()]; + for (int i = 0; i < methods.length; i++) { + methods[i] = methodList.get(i); + } + return methods; + } + + private static void addMethodIfNotExist(List allMethods, Method newMethod) { + for (Method method : allMethods) { + if (ObjectUtil.isEquals(method, newMethod)) { + return; + } + } + + allMethods.add(newMethod); + } + + public static Field getField(Class clazz, String fieldName) { + if (ObjectUtil.isAnyNull(clazz, fieldName)) { + return null; + } + + return getField0(clazz, fieldName); + } + + static Field getField0(Class clazz, String fieldName) { + for (Class itr = clazz; hasSuperClass(itr);) { + Field[] fields = itr.getDeclaredFields(); + for (Field field : fields) { + if (field.getName().equals(fieldName)) { + return field; + } + } + + itr = itr.getSuperclass(); + } + + return null; + } + public static Class getComponentType(Type type, Class implClass) { + Class[] componentTypes = getComponentTypes(type, implClass); + if (componentTypes == null) { + return null; + } + return componentTypes[componentTypes.length - 1]; + } + + public static Class getComponentType(Type type) { + return getComponentType(type, null); + } + + static Field[] getAllFieldsOfClass0(Class clazz) { + Field[] fields = null; + + for (Class itr = clazz; hasSuperClass(itr);) { + fields = ArrayUtil.addAll(itr.getDeclaredFields(), fields); + itr = itr.getSuperclass(); + } + + return fields; + } + + public static boolean hasSuperClass(Class clazz) { + return (clazz != null) && !clazz.equals(Object.class); + } + + public static Annotation[] getAnnotation(AnnotatedElement annotatedElement) { + if (Objects.isNull(annotatedElement)) { + return null; + } + + return annotatedElement.getAnnotations(); + } + + public static boolean isPublic(Member m) { + return m != null && Modifier.isPublic(m.getModifiers()); + } + + public static boolean isAccessible(Member m) { + return m != null && Modifier.isPublic(m.getModifiers()); + } + + public static void forceAccess(AccessibleObject object) { + if (object == null || object.isAccessible()) { + return; + } + try { + object.setAccessible(true); + } catch (SecurityException e) { + throw new RuntimeException(e); + } + } + + public static Class getRawType(Type type) { + return getRawType(type, null); + } + + public static Class getRawType(Type type, Class implClass) { + if (type == null) { + return null; + } + + GenericType gt = GenericType.find(type); + if (gt != null) { + return gt.toRawType(type, implClass); + } + + return null; + + } + + public static Class[] getComponentTypes(Type type, Class implClass) { + if (type == null) { + return null; + } + + GenericType gt = GenericType.find(type); + if (gt != null) { + return gt.getComponentTypes(type, implClass); + } + + return null; + + } + + public static Field[] getAccessibleFields(Class clazz) { + return getAccessibleFields(clazz, Object.class); + } + + public static Field[] getAccessibleFields(Class clazz, Class limit) { + if (clazz == null) { + return null; + } + + Package topPackage = clazz.getPackage(); + List fieldList = new ArrayList<>(); + int topPackageHash = (topPackage == null) ? 0 : topPackage.hashCode(); + boolean top = true; + do { + if (clazz == null) { + break; + } + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + if (top == true) { // add all top declared fields + fieldList.add(field); + continue; + } + int modifier = field.getModifiers(); + if (Modifier.isPrivate(modifier)) { + continue; + } + if (Modifier.isPublic(modifier) || Modifier.isProtected(modifier)) { + addFieldIfNotExist(fieldList, field); + continue; + } + + // add super default methods from the same package + Package pckg = field.getDeclaringClass().getPackage(); + int pckgHash = (pckg == null) ? 0 : pckg.hashCode(); + if (pckgHash == topPackageHash) { + addFieldIfNotExist(fieldList, field); + } + } + top = false; + } while ((clazz = clazz.getSuperclass()) != limit); + + Field[] fields = new Field[fieldList.size()]; + for (int i = 0; i < fields.length; i++) { + fields[i] = fieldList.get(i); + } + + return fields; + } + + private static void addFieldIfNotExist(List allFields, Field newField) { + for (Field field : allFields) { + if (ObjectUtil.isEquals(field, newField)) { + return; + } + } + + allFields.add(newField); + } + + + enum GenericType { + + CLASS_TYPE { + + @Override + Class type() { + return Class.class; + } + + @Override + Class toRawType(Type type, Class implClass) { + return (Class) type; + } + + @Override + Class[] getComponentTypes(Type type, Class implClass) { + Class clazz = (Class) type; + if (clazz.isArray()) { + return new Class[] { clazz.getComponentType() }; + } + return null; + } + }, + PARAMETERIZED_TYPE { + + @Override + Class type() { + return ParameterizedType.class; + } + + @Override + Class toRawType(Type type, Class implClass) { + ParameterizedType pType = (ParameterizedType) type; + return getRawType(pType.getRawType(), implClass); + } + + @Override + Class[] getComponentTypes(Type type, Class implClass) { + ParameterizedType pt = (ParameterizedType) type; + + Type[] generics = pt.getActualTypeArguments(); + + if (generics.length == 0) { + return null; + } + + Class[] types = new Class[generics.length]; + + for (int i = 0; i < generics.length; i++) { + types[i] = getRawType(generics[i], implClass); + } + return types; + } + }, + WILDCARD_TYPE { + + @Override + Class type() { + return WildcardType.class; + } + + @Override + Class toRawType(Type type, Class implClass) { + WildcardType wType = (WildcardType) type; + + Type[] lowerTypes = wType.getLowerBounds(); + if (lowerTypes.length > 0) { + return getRawType(lowerTypes[0], implClass); + } + + Type[] upperTypes = wType.getUpperBounds(); + if (upperTypes.length != 0) { + return getRawType(upperTypes[0], implClass); + } + + return Object.class; + } + + @Override + Class[] getComponentTypes(Type type, Class implClass) { + return null; + } + }, + GENERIC_ARRAY_TYPE { + + @Override + Class type() { + return GenericArrayType.class; + } + + @Override + Class toRawType(Type type, Class implClass) { + Type genericComponentType = ((GenericArrayType) type).getGenericComponentType(); + Class rawType = getRawType(genericComponentType, implClass); + // FIXME + return Array.newInstance(rawType, 0).getClass(); + } + + @Override + Class[] getComponentTypes(Type type, Class implClass) { + GenericArrayType gat = (GenericArrayType) type; + + Class rawType = getRawType(gat.getGenericComponentType(), implClass); + if (rawType == null) { + return null; + } + + return new Class[] { rawType }; + } + }, + TYPE_VARIABLE { + + @Override + Class type() { + return TypeVariable.class; + } + + @Override + Class toRawType(Type type, Class implClass) { + TypeVariable varType = (TypeVariable) type; + if (implClass != null) { + Type resolvedType = resolveVariable(varType, implClass); + if (resolvedType != null) { + return getRawType(resolvedType, null); + } + } + Type[] boundsTypes = varType.getBounds(); + if (boundsTypes.length == 0) { + return Object.class; + } + return getRawType(boundsTypes[0], implClass); + } + + @Override + Class[] getComponentTypes(Type type, Class implClass) { + return null; + } + }; + + abstract Class toRawType(Type type, Class implClass); + + abstract Class type(); + + abstract Class[] getComponentTypes(Type type, Class implClass); + + static GenericType find(Type type) { + for (GenericType gt : GenericType.values()) { + if (gt.type().isInstance(type)) { + return gt; + } + } + + return null; + } + } + + public static Type resolveVariable(TypeVariable variable, final Class implClass) { + final Class rawType = getRawType(implClass, null); + + int index = ArrayUtil.indexOf(rawType.getTypeParameters(), variable); + if (index >= 0) { + return variable; + } + + final Class[] interfaces = rawType.getInterfaces(); + final Type[] genericInterfaces = rawType.getGenericInterfaces(); + + for (int i = 0; i <= interfaces.length; i++) { + Class rawInterface; + + if (i < interfaces.length) { + rawInterface = interfaces[i]; + } else { + rawInterface = rawType.getSuperclass(); + if (rawInterface == null) { + continue; + } + } + + final Type resolved = resolveVariable(variable, rawInterface); + if (resolved instanceof Class || resolved instanceof ParameterizedType) { + return resolved; + } + + if (resolved instanceof TypeVariable) { + final TypeVariable typeVariable = (TypeVariable) resolved; + index = ArrayUtil.indexOf(rawInterface.getTypeParameters(), typeVariable); + + if (index < 0) { + throw new IllegalArgumentException("Invalid type variable:" + typeVariable); + } + + final Type type = i < genericInterfaces.length ? genericInterfaces[i] : rawType.getGenericSuperclass(); + + if (type instanceof Class) { + return Object.class; + } + + if (type instanceof ParameterizedType) { + return ((ParameterizedType) type).getActualTypeArguments()[index]; + } + + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + return null; + } + + public static Method getMethod(Class clazz, String methodName, Class...parameterTypes) { + if (clazz == null || methodName == null) { + return null; + } + + for (Class itr = clazz; hasSuperClass(itr);) { + Method[] methods = itr.getDeclaredMethods(); + + for (Method method : methods) { + if (method.getName().equals(methodName) && Arrays.equals(method.getParameterTypes(), parameterTypes)) { + return method; + } + } + + itr = itr.getSuperclass(); + } + + return null; + + } + + public static Field[] getAllInstanceFields(Class clazz) { + if (clazz == null) { + return null; + } + + return getAllInstanceFields0(clazz); + } + + static Field[] getAllInstanceFields0(Class clazz) { + List fields = new ArrayList<>(); + for (Class itr = clazz; hasSuperClass(itr);) { + for (Field field : itr.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers())) { + fields.add(field); + } + } + itr = itr.getSuperclass(); + } + + return fields.toArray(new Field[fields.size()]); + } + + public static List getAnnotationMethods(Class clazz, Class annotationType) { + if (clazz == null || annotationType == null) { + return null; + } + List list = new ArrayList<>(); + + for (Method method : getAllMethodsOfClass(clazz)) { + A type = method.getAnnotation(annotationType); + if (type != null) { + list.add(method); + } + } + + return list; + } + + public static Field[] getAnnotationFields(Class clazz, Class annotationClass) { + if (clazz == null || annotationClass == null) { + return null; + } + + Field[] fields = getAllFieldsOfClass0(clazz); + if (ArrayUtil.isEmpty(fields)) { + return null; + } + + List list = new ArrayList<>(); + for (Field field : fields) { + if (null != field.getAnnotation(annotationClass)) { + list.add(field); + field.setAccessible(true); + } + } + + return list.toArray(new Field[0]); + } + + public static Class[] getGenericSuperTypes(Class type) { + if (type == null) { + return null; + } + + return getComponentTypes(type.getGenericSuperclass()); + } + + public static Class[] getComponentTypes(Type type) { + return getComponentTypes(type, null); + } + + public static T invokeMethod(Method method, Object target, Object...args) { + if (method == null) { + return null; + } + + method.setAccessible(true); + try { + @SuppressWarnings("unchecked") + T result = (T) method.invoke(target, args); + + return result; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + } + + public static Object invokeMethod(Object object, String methodName, Class[] parameterTypes, Object...args) { + if (object == null || methodName == null) { + return null; + } + + if (parameterTypes == null) { + parameterTypes = new Class[0]; + } + if (args == null) { + args = new Object[0]; + } + Method method; + try { + method = object.getClass().getDeclaredMethod(methodName, parameterTypes); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + if (method == null) { + return null; + } + + return invokeMethod(method, object, args); + + } + + public static Object invokeMethod(Object object, String methodName, Object...args) { + if (object == null || methodName == null) { + return null; + } + if (args == null) { + args = new Object[0]; + } + + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + + return invokeMethod(object, methodName, parameterTypes, args); + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Setter.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Setter.java new file mode 100644 index 000000000..2b14ba947 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/introspection/Setter.java @@ -0,0 +1,12 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import java.lang.reflect.InvocationTargetException; + +public interface Setter { + + void invokeSetter(Object target, Object argument) throws IllegalAccessException, InvocationTargetException; + + Class getSetterRawType(); + + Class getSetterRawComponentType(); +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutor.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutor.java index 6d0af186a..553485498 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutor.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutor.java @@ -39,6 +39,7 @@ public interface GraphQLExecutor { * Execute GraphQL query provided in query argument and variables * * @param query GraphQL query string + * @param arguments GraphQL arguments key/value mapo * @return GraphQL ExecutionResult */ ExecutionResult execute(String query, Map arguments); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java index faf6321c4..d2753080b 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java @@ -25,7 +25,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Collections; @@ -38,6 +41,9 @@ import java.util.UUID; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import graphql.Assert; import graphql.Scalars; import graphql.language.ArrayValue; @@ -55,8 +61,6 @@ import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; import graphql.schema.GraphQLScalarType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Provides Registry to resolve GraphQL Query Java Scalar Types @@ -111,7 +115,10 @@ public class JavaScalars { scalarsRegistry.put(Object.class, new GraphQLScalarType("Object", "Object type", new GraphQLObjectCoercing())); scalarsRegistry.put(java.sql.Date.class, new GraphQLScalarType("SqlDate", "SQL Date type", new GraphQLSqlDateCoercing())); scalarsRegistry.put(java.sql.Timestamp.class, new GraphQLScalarType("SqlTimestamp", "SQL Timestamp type", new GraphQLSqlTimestampCoercing())); - scalarsRegistry.put(Byte[].class, new GraphQLScalarType("ByteArray", "ByteArray type", new GraphQLLOBCoercing())); + scalarsRegistry.put(Byte[].class, new GraphQLScalarType("ByteArray", "ByteArray type", new GraphQLLOBCoercing())); + scalarsRegistry.put(Instant.class, new GraphQLScalarType("Instant", "Instant type", new GraphQLInstantCoercing())); + scalarsRegistry.put(ZonedDateTime.class, new GraphQLScalarType("ZonedDateTime", "ZonedDateTime type", new GraphQLZonedDateTimeCoercing())); + scalarsRegistry.put(OffsetDateTime.class, new GraphQLScalarType("OffsetDateTime", "OffsetDateTime type", new GraphQLOffsetDateTimeCoercing())); } public static GraphQLScalarType of(Class key) { @@ -142,6 +149,8 @@ public Object serialize(Object input) { return parseStringToLocalDateTime((String) input); } else if (input instanceof LocalDateTime) { return input; + }else if (input instanceof LocalDate) { + return input; } else if (input instanceof Long) { return parseLongToLocalDateTime((Long) input); } else if (input instanceof Integer) { @@ -331,6 +340,131 @@ private Date parseStringToDate(String input) { } }; + public static class GraphQLInstantCoercing implements Coercing { + + @Override + public Object serialize(Object input) { + if (input instanceof String) { + return parseStringToInstant((String) input); + } else if (input instanceof Instant) { + return input; + } + return null; + } + + @Override + public Object parseValue(Object input) { + return serialize(input); + } + + @Override + public Object parseLiteral(Object input) { + if (input instanceof StringValue) { + return parseStringToInstant(((StringValue) input).getValue()); + } + return null; + } + + private Instant parseStringToInstant(String input) { + try { + return Instant.parse(input); + } catch (DateTimeParseException e) { + log.warn("Failed to parse Date from input: " + input, e); + return null; + } + } + }; + + public static class GraphQLZonedDateTimeCoercing implements Coercing { + + @Override + public Object serialize(Object input) { + if (input instanceof String) { + return parseStringToZonedDateTime((String) input); + } else if (input instanceof ZonedDateTime) { + return ((ZonedDateTime) input).withZoneSameInstant(ZoneId.of("UTC")); + } else if (input instanceof LocalDate) { + return input; + } else if (input instanceof Long) { + return parseLongToZonedDateTime((Long) input); + } else if (input instanceof Integer) { + return parseLongToZonedDateTime((Integer) input); + } + return null; + } + + @Override + public Object parseValue(Object input) { + return serialize(input); + } + + @Override + public Object parseLiteral(Object input) { + if (input instanceof StringValue) { + return parseStringToZonedDateTime(((StringValue) input).getValue()); + } + return null; + } + + private ZonedDateTime parseLongToZonedDateTime(long input) { + return ZonedDateTime.ofInstant(Instant.ofEpochSecond(input), TimeZone.getDefault().toZoneId()); + } + + private ZonedDateTime parseStringToZonedDateTime(String input) { + try { + return ZonedDateTime.parse(input); + } catch (DateTimeParseException e) { + log.warn("Failed to parse Date from input: " + input, e); + return null; + } + } + }; + + public static class GraphQLOffsetDateTimeCoercing implements Coercing { + + @Override + public Object serialize(Object input) { + if (input instanceof String) { + return parseStringToOffsetDateTime((String) input); + } else if (input instanceof OffsetDateTime) { + return ((OffsetDateTime) input).withOffsetSameInstant(ZoneOffset.of("Z")); + } else if (input instanceof LocalDate) { + return input; + } else if (input instanceof Long) { + return parseLongToOffsetDateTime((Long) input); + } else if (input instanceof Integer) { + return parseLongToOffsetDateTime((Integer) input); + } + return null; + } + + @Override + public Object parseValue(Object input) { + return serialize(input); + } + + @Override + public Object parseLiteral(Object input) { + if (input instanceof StringValue) { + return parseStringToOffsetDateTime(((StringValue) input).getValue()); + } + return null; + } + + private OffsetDateTime parseLongToOffsetDateTime(long input) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(input), TimeZone.getDefault().toZoneId()); + } + + private OffsetDateTime parseStringToOffsetDateTime(String input) { + try { + return OffsetDateTime.parse(input); + } catch (DateTimeParseException e) { + log.warn("Failed to parse Date from input: " + input, e); + return null; + } + } + }; + public static class GraphQLUUIDCoercing implements Coercing { @Override diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java new file mode 100644 index 000000000..b2ec64bee --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java @@ -0,0 +1,429 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import static java.util.Locale.ENGLISH; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.ManagedType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; +import com.introproventures.graphql.jpa.query.introspection.ClassDescriptor; +import com.introproventures.graphql.jpa.query.introspection.ClassIntrospector; +import com.introproventures.graphql.jpa.query.introspection.FieldDescriptor; +import com.introproventures.graphql.jpa.query.introspection.MethodDescriptor; +import com.introproventures.graphql.jpa.query.introspection.PropertyDescriptor; + +public class EntityIntrospector { + private static final Logger LOGGER = LoggerFactory.getLogger(EntityIntrospector.class); + + private static final Map, EntityIntrospectionResult> map = new LinkedHashMap<>(); + + private static ClassIntrospector introspector = ClassIntrospector.builder() + .withScanAccessible(false) + .withEnhancedProperties(true) + .withIncludeFieldsAsProperties(false) + .withScanStatics(false) + .build(); + /** + * Get existing EntityIntrospectionResult for Java type + * + * @param entity Java type of the entity + * @return EntityIntrospectionResult result + * @throws NoSuchElementException if not found + */ + public static EntityIntrospectionResult resultOf(Class entity) { + return Optional.ofNullable(map.get(entity)) + .orElseThrow(() -> new NoSuchElementException(entity.getName())); + } + + /** + * Introspect entity type represented by ManagedType instance + * + * @param entityType ManagedType representing persistent entity + * @return EntityIntrospectionResult result + */ + public static EntityIntrospectionResult introspect(ManagedType entityType) { + return map.computeIfAbsent(entityType.getJavaType(), + cls -> new EntityIntrospectionResult(entityType)); + } + + public static class EntityIntrospectionResult { + + private final Map descriptors; + private final Class entity; + private final ClassDescriptor classDescriptor; + private final ManagedType managedType; + private final Map> attributes; + + public EntityIntrospectionResult(ManagedType managedType) { + this.managedType = managedType; + + this.attributes = managedType.getAttributes() + .stream() + .collect(Collectors.toMap(Attribute::getName, + Function.identity())); + + this.entity = managedType.getJavaType(); + + this.classDescriptor = introspector.introspect(entity); + + this.descriptors = Stream.of(classDescriptor.getAllPropertyDescriptors()) + .filter(it -> !"class".equals(it.getName())) + .map(AttributePropertyDescriptor::new) + .collect(Collectors.toMap(AttributePropertyDescriptor::getName, + Function.identity())); + } + + public Collection getTransientPropertyDescriptors() { + return descriptors.values() + .stream() + .filter(AttributePropertyDescriptor::isTransient) + .collect(Collectors.toList()); + } + + public Collection getPersistentPropertyDescriptors() { + return descriptors.values() + .stream() + .filter(AttributePropertyDescriptor::isPersistent) + .collect(Collectors.toList()); + } + + public Collection getIgnoredPropertyDescriptors() { + return descriptors.values() + .stream() + .filter(AttributePropertyDescriptor::isIgnored) + .collect(Collectors.toList()); + } + + public Map> getAttributes() { + return attributes; + } + + /** + * Test if entity property is annotated with GraphQLIgnore + * + * @param propertyName the name of the property + * @return true if property has GraphQLIgnore annotation + * @throws NoSuchElementException if property does not exists + */ + public Boolean isIgnored(String propertyName) { + return getPropertyDescriptor(propertyName).map(AttributePropertyDescriptor::isIgnored) + .orElseThrow(() -> noSuchElementException(entity, propertyName)); + } + + /** + * Test if entity property is not ignored + * + * @param propertyName the name of the property + * @return true if property has no GraphQLIgnore annotation + * @throws NoSuchElementException if property does not exists + */ + public Boolean isNotIgnored(String propertyName) { + return getPropertyDescriptor(propertyName).map(AttributePropertyDescriptor::isNotIgnored) + .orElseThrow(() -> noSuchElementException(entity, propertyName)); + } + + public Collection getPropertyDescriptors() { + return descriptors.values(); + } + + public Optional getPropertyDescriptor(String fieldName) { + return Optional.ofNullable(descriptors.get(fieldName)); + } + + public Optional getPropertyDescriptor(Attribute attribute) { + return getPropertyDescriptor(attribute.getName()); + } + + public boolean hasPropertyDescriptor(String fieldName) { + return descriptors.containsKey(fieldName); + } + + /** + * Test if Java bean property is transient according to JPA specification + * + * @param propertyName the name of the property + * @return true if property has Transient annotation or transient field modifier + * @throws NoSuchElementException if property does not exists + */ + public Boolean isTransient(String propertyName) { + return getPropertyDescriptor(propertyName).map(AttributePropertyDescriptor::isTransient) + .orElseThrow(() -> noSuchElementException(entity, propertyName)); + } + + /** + * Test if Java bean property is persistent according to JPA specification + * + * @param propertyName the name of the property + * @return true if property is persitent + * @throws NoSuchElementException if property does not exists + */ + public Boolean isPersistent(String propertyName) { + return !isTransient(propertyName); + } + + public Class getEntity() { + return entity; + } + + public ManagedType getManagedType() { + return managedType; + } + + public ClassDescriptor getClassDescriptor() { + return classDescriptor; + } + + public Optional getSchemaDescription() { + return getClasses().filter(cls -> !Object.class.equals(cls)) + .map(cls -> Optional.ofNullable(cls.getAnnotation(GraphQLDescription.class)) + .map(GraphQLDescription::value)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + + public boolean hasSchemaDescription() { + return getSchemaDescription().isPresent(); + } + + public Optional getSchemaDescription(String propertyName) { + return getPropertyDescriptor(propertyName).flatMap(AttributePropertyDescriptor::getSchemaDescription); + } + + public Stream> getClasses() { + return iterate(entity, k -> Optional.ofNullable(k.getSuperclass())); + } + + public class AttributePropertyDescriptor { + + private final PropertyDescriptor delegate; + private final Optional> attribute; + private final Optional field; + private final Optional readMethod; + + public AttributePropertyDescriptor(PropertyDescriptor delegate) { + this.delegate = delegate; + + String name = delegate.getName(); + + this.readMethod = Optional.ofNullable(delegate.getReadMethodDescriptor()) + .map(MethodDescriptor::getMethod) + .filter(m -> !Modifier.isPrivate(m.getModifiers())); + + this.attribute = Optional.ofNullable(attributes.getOrDefault(name, + attributes.get(capitalize(name)))); + this.field = attribute.map(Attribute::getJavaMember) + .filter(Field.class::isInstance) + .map(Field.class::cast) + .map(Optional::of) + .orElseGet(() -> Optional.ofNullable(delegate.getFieldDescriptor()) + .map(FieldDescriptor::getField)); + } + + public ManagedType getManagedType() { + return managedType; + } + + public PropertyDescriptor getDelegate() { + return delegate; + } + + public Class getPropertyType() { + return delegate.getType(); + } + + public String getName() { + return attribute.map(Attribute::getName) + .orElseGet(() -> delegate.getName()); + } + + public Optional getReadMethod() { + return readMethod; + } + + public Optional getAnnotation(Class annotationClass) { + return getReadMethod().map(m -> m.getAnnotation(annotationClass)) + .map(Optional::of).orElseGet(() -> getField().map(f -> f.getAnnotation(annotationClass))); + } + + public Optional> getAttribute() { + return attribute; + } + + public Optional getField() { + return field; + } + + public Optional getSchemaDescription() { + return getAnnotation(GraphQLDescription.class).map(GraphQLDescription::value); + } + + public boolean hasSchemaDescription() { + return getSchemaDescription().isPresent(); + } + + public boolean isTransient() { + return !attribute.isPresent(); + } + + public boolean isPersistent() { + return attribute.isPresent(); + } + + public boolean isIgnored() { + return isAnnotationPresent(GraphQLIgnore.class); + } + + public boolean isNotIgnored() { + return !isIgnored(); + } + + public boolean hasReadMethod() { + return getReadMethod().isPresent(); + } + + public boolean isAnnotationPresent(Class annotation) { + return getAnnotation(annotation).isPresent(); + } + + @Override + public String toString() { + return "AttributePropertyDescriptor [delegate=" + delegate + "]"; + } + + private EntityIntrospectionResult getEnclosingInstance() { + return EntityIntrospectionResult.this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getEnclosingInstance().hashCode(); + result = prime * result + Objects.hash(delegate); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AttributePropertyDescriptor other = (AttributePropertyDescriptor) obj; + if (!getEnclosingInstance().equals(other.getEnclosingInstance())) + return false; + return Objects.equals(delegate, other.delegate); + } + } + + + @Override + public int hashCode() { + return Objects.hash(classDescriptor, entity); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + EntityIntrospectionResult other = (EntityIntrospectionResult) obj; + return Objects.equals(classDescriptor, other.classDescriptor) && Objects.equals(entity, other.entity); + } + + @Override + public String toString() { + return "EntityIntrospectionResult [beanInfo=" + classDescriptor + "]"; + } + } + + /** + * The following method is borrowed from Streams.iterate, + * however Streams.iterate is designed to create infinite streams. + * + * This version has been modified to end when Optional.empty() + * is returned from the fetchNextFunction. + + * @param the type of stream elements + * @param seed the initial element + * @param f a function to be applied to the previous element to produce + * a new element + * @return a new sequential {@code Stream} + * + */ + public static Stream iterate(T seed, Function> f) { + Objects.requireNonNull(f); + + Iterator iterator = new Iterator() { + private Optional t = Optional.ofNullable(seed); + + @Override + public boolean hasNext() { + return t.isPresent(); + } + + @Override + public T next() { + T v = t.get(); + + t = f.apply(v); + + return v; + } + }; + + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED | Spliterator.IMMUTABLE), + false + ); + } + + private static NoSuchElementException noSuchElementException(Class containerClass, + String propertyName) { + return new NoSuchElementException(String.format(Locale.ROOT, + "Could not locate field name [%s] on class [%s]", + propertyName, + containerClass.getName())); + + } + + /** + * Returns a String which capitalizes the first letter of the string. + */ + private static String capitalize(String name) { + if (name == null || name.length() == 0) { + return name; + } + return name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index d52f1287c..8b0bbc460 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -30,7 +30,6 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.metamodel.EntityType; -import javax.persistence.metamodel.SingularAttribute; import graphql.language.Argument; import graphql.language.BooleanValue; @@ -109,10 +108,6 @@ public Object get(DataFetchingEnvironment environment) { queryField = new Field(fieldName, field.getArguments(), recordsSelection.get().getSelectionSet()); - // Let's clear session persistent context to avoid getting stale objects cached in the same session - // between requests with different search criteria. This looks like a Hibernate bug... - entityManager.clear(); - TypedQuery query = getQuery(queryEnvironment, queryField, isDistinct); // Let's apply page only if present @@ -187,12 +182,14 @@ private TypedQuery getCountQuery(DataFetchingEnvironment environment, Fiel CriteriaQuery query = cb.createQuery(Long.class); Root root = query.from(entityType); - SingularAttribute idAttribute = entityType.getId(Object.class); + DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .root(query) + .build(); - query.select(cb.count(root.get(idAttribute.getName()))); + query.select(cb.count(root)); List predicates = field.getArguments().stream() - .map(it -> getPredicate(cb, root, null, environment, it)) + .map(it -> getPredicate(cb, root, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); 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 ae145b580..8e7708f55 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,14 +16,9 @@ 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; @@ -35,7 +30,6 @@ import javax.persistence.Convert; import javax.persistence.EntityManager; -import javax.persistence.Transient; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EmbeddableType; import javax.persistence.metamodel.EntityType; @@ -44,15 +38,18 @@ import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; -import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreFilter; import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnoreOrder; import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder; import com.introproventures.graphql.jpa.query.schema.JavaScalars; import com.introproventures.graphql.jpa.query.schema.NamingStrategy; -import com.introproventures.graphql.jpa.query.schema.impl.IntrospectionUtils.CachedIntrospectionResult.CachedPropertyDescriptor; +import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; + import graphql.Assert; import graphql.Scalars; import graphql.schema.Coercing; @@ -71,8 +68,6 @@ import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeReference; import graphql.schema.PropertyDataFetcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * JPA specific schema builder implementation of {code #GraphQLSchemaBuilder} interface @@ -163,7 +158,7 @@ private GraphQLObjectType getQueryType() { private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType entityType) { return GraphQLFieldDefinition.newFieldDefinition() .name(entityType.getName()) - .description(getSchemaDescription( entityType.getJavaType())) + .description(getSchemaDescription(entityType)) .type(getObjectType(entityType)) .dataFetcher(new GraphQLJpaSimpleDataFetcher(entityManager, entityType, toManyDefaultOptional)) .argument(entityType.getAttributes().stream() @@ -425,7 +420,7 @@ private GraphQLInputObjectField getWhereInputRelationField(Attribute attrib ManagedType foreignType = getForeignType(attribute); String type = resolveWhereInputTypeName(foreignType); - String description = getSchemaDescription(attribute.getJavaMember()); + String description = getSchemaDescription(attribute); return GraphQLInputObjectField.newInputObjectField() .name(attribute.getName()) @@ -436,7 +431,7 @@ private GraphQLInputObjectField getWhereInputRelationField(Attribute attrib private GraphQLInputObjectField getWhereInputField(Attribute attribute) { GraphQLInputType type = getWhereAttributeType(attribute); - String description = getSchemaDescription(attribute.getJavaMember()); + String description = getSchemaDescription(attribute); if (type instanceof GraphQLInputType) { return GraphQLInputObjectField.newInputObjectField() @@ -598,7 +593,7 @@ else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) private GraphQLArgument getArgument(Attribute attribute) { GraphQLInputType type = getAttributeInputType(attribute); - String description = getSchemaDescription(attribute.getJavaMember()); + String description = getSchemaDescription(attribute); return GraphQLArgument.newArgument() .name(attribute.getName()) @@ -618,7 +613,7 @@ private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean if (input) { graphQLType = GraphQLInputObjectType.newInputObject() .name(embeddableTypeName) - .description(getSchemaDescription(embeddableType.getJavaType())) + .description(getSchemaDescription(embeddableType)) .fields(embeddableType.getAttributes().stream() .filter(this::isNotIgnored) .map(this::getInputObjectField) @@ -628,7 +623,7 @@ private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean } else { graphQLType = GraphQLObjectType.newObject() .name(embeddableTypeName) - .description(getSchemaDescription(embeddableType.getJavaType())) + .description(getSchemaDescription(embeddableType)) .fields(embeddableType.getAttributes().stream() .filter(this::isNotIgnored) .map(this::getObjectField) @@ -654,45 +649,46 @@ private GraphQLObjectType getObjectType(EntityType entityType) { private GraphQLObjectType computeObjectType(EntityType entityType) { return GraphQLObjectType.newObject() .name(entityType.getName()) - .description(getSchemaDescription(entityType.getJavaType())) + .description(getSchemaDescription(entityType)) .fields(getEntityAttributesFields(entityType)) - .fields(getTransientFields(entityType.getJavaType())) + .fields(getTransientFields(entityType)) .build(); } private List getEntityAttributesFields(EntityType entityType) { - return entityType.getAttributes() - .stream() - .filter(this::isNotIgnored) - .map(it -> getObjectField(it, entityType)) - .collect(Collectors.toList()); - } - - - private List getTransientFields(Class clazz) { - return IntrospectionUtils.introspect(clazz) - .getPropertyDescriptors().stream() - .filter(it -> it.isAnnotationPresent(Transient.class)) - .map(CachedPropertyDescriptor::getDelegate) - .filter(it -> isNotIgnored(it.getPropertyType())) - .map(this::getJavaFieldDefinition) - .collect(Collectors.toList()); + return entityType.getAttributes() + .stream() + .filter(attribute -> EntityIntrospector.introspect(entityType) + .isNotIgnored(attribute.getName())) + .map(it -> getObjectField(it, entityType)) + .collect(Collectors.toList()); + } + + private List getTransientFields(ManagedType managedType) { + return EntityIntrospector.introspect(managedType) + .getTransientPropertyDescriptors() + .stream() + .filter(AttributePropertyDescriptor::isNotIgnored) + .map(this::getJavaFieldDefinition) + .collect(Collectors.toList()); } @SuppressWarnings( { "rawtypes" } ) - private GraphQLFieldDefinition getJavaFieldDefinition(PropertyDescriptor propertyDescriptor) { + private GraphQLFieldDefinition getJavaFieldDefinition(AttributePropertyDescriptor propertyDescriptor) { GraphQLOutputType type = getGraphQLTypeFromJavaType(propertyDescriptor.getPropertyType()); DataFetcher dataFetcher = PropertyDataFetcher.fetching(propertyDescriptor.getName()); + + String description = propertyDescriptor.getSchemaDescription().orElse(null); return GraphQLFieldDefinition.newFieldDefinition() - .name(propertyDescriptor.getName()) - .description(getSchemaDescription(propertyDescriptor.getPropertyType())) - .type(type) - .dataFetcher(dataFetcher) - .build(); + .name(propertyDescriptor.getName()) + .description(description) + .type(type) + .dataFetcher(dataFetcher) + .build(); } - private GraphQLFieldDefinition getObjectField(Attribute attribute) { + private GraphQLFieldDefinition getObjectField(Attribute attribute) { return getObjectField(attribute, null); } @@ -750,7 +746,7 @@ else if (attribute instanceof PluralAttribute return GraphQLFieldDefinition.newFieldDefinition() .name(attribute.getName()) - .description(getSchemaDescription(attribute.getJavaMember())) + .description(getSchemaDescription(attribute)) .type(type) .dataFetcher(dataFetcher) .argument(arguments) @@ -779,7 +775,7 @@ private GraphQLInputObjectField getInputObjectField(Attribute attribute) { return GraphQLInputObjectField.newInputObjectField() .name(attribute.getName()) - .description(getSchemaDescription(attribute.getJavaMember())) + .description(getSchemaDescription(attribute)) .type(type) .build(); } @@ -877,86 +873,25 @@ protected final boolean isValidAssociation(Attribute attribute) { return isOneToMany(attribute) || isToOne(attribute); } - - - private String getSchemaDescription(Member member) { - if (member instanceof AnnotatedElement) { - 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 String getSchemaDescription(Attribute attribute) { + return EntityIntrospector.introspect(attribute.getDeclaringType()) + .getSchemaDescription(attribute.getName()) + .orElse(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; + + private String getSchemaDescription(EntityType entityType) { + return EntityIntrospector.introspect(entityType) + .getSchemaDescription() + .orElse(null); } - private String getSchemaDescription(AnnotatedElement annotatedElement) { - if (annotatedElement != null) { - GraphQLDescription schemaDocumentation = annotatedElement.getAnnotation(GraphQLDescription.class); - return schemaDocumentation != null ? schemaDocumentation.value() : null; - } - - return null; + private String getSchemaDescription(EmbeddableType embeddableType) { + return EntityIntrospector.introspect(embeddableType) + .getSchemaDescription() + .orElse(null); } - + private boolean isNotIgnored(EmbeddableType attribute) { return isNotIgnored(attribute.getJavaType()); } @@ -978,12 +913,7 @@ private boolean isNotIgnored(Member member) { } private boolean isNotIgnored(AnnotatedElement annotatedElement) { - if (annotatedElement != null) { - GraphQLIgnore schemaDocumentation = annotatedElement.getAnnotation(GraphQLIgnore.class); - return schemaDocumentation == null; - } - - return false; + return annotatedElement != null && annotatedElement.getAnnotation(GraphQLIgnore.class) == null; } protected boolean isNotIgnoredFilter(Attribute attribute) { @@ -1035,6 +965,8 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { classCache.putIfAbsent(clazz, enumType); return enumType; + } else if (clazz.isArray()) { + return GraphQLList.list(JavaScalars.of(clazz.getComponentType())); } return JavaScalars.of(clazz); @@ -1095,8 +1027,8 @@ private void setNoOpCoercing(GraphQLType type) { GraphQLEnumType.newEnum() .name("OrderBy") .description("Specifies the direction (Ascending / Descending) to sort a field.") - .value("ASC", 0, "Ascending") - .value("DESC", 1, "Descending") + .value("ASC", "ASC", "Ascending") + .value("DESC", "DESC", "Descending") .build(); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java deleted file mode 100644 index 1cc9098f3..000000000 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java +++ /dev/null @@ -1,96 +0,0 @@ -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.annotation.Annotation; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.persistence.Transient; - -public class IntrospectionUtils { - private static final Map, CachedIntrospectionResult> map = new LinkedHashMap<>(); - - public static CachedIntrospectionResult introspect(Class entity) { - return map.computeIfAbsent(entity, CachedIntrospectionResult::new); - } - - public static boolean isTransient(Class entity, String propertyName) { - return introspect(entity).getPropertyDescriptor(propertyName) - .map(it -> it.isAnnotationPresent(Transient.class)) - .orElseThrow(() -> new RuntimeException(new NoSuchFieldException(propertyName))); - } - - public static class CachedIntrospectionResult { - - private final Map map; - private final Class entity; - private final BeanInfo beanInfo; - - public CachedIntrospectionResult(Class entity) { - try { - this.beanInfo = Introspector.getBeanInfo(entity); - } catch (IntrospectionException cause) { - throw new RuntimeException(cause); - } - - this.entity = entity; - this.map = Stream.of(beanInfo.getPropertyDescriptors()) - .map(CachedPropertyDescriptor::new) - .collect(Collectors.toMap(CachedPropertyDescriptor::getName, it -> it)); - } - - public Collection getPropertyDescriptors() { - return map.values(); - } - - public Optional getPropertyDescriptor(String fieldName) { - return Optional.ofNullable(map.getOrDefault(fieldName, null)); - } - - public Class getEntity() { - return entity; - } - - public BeanInfo getBeanInfo() { - return beanInfo; - } - - public class CachedPropertyDescriptor { - private final PropertyDescriptor delegate; - - public CachedPropertyDescriptor(PropertyDescriptor delegate) { - this.delegate = delegate; - } - - public PropertyDescriptor getDelegate() { - return delegate; - } - - public String getName() { - return delegate.getName(); - } - - public boolean isAnnotationPresent(Class annotation) { - boolean answer; - try { - answer = entity.getDeclaredField(delegate.getName()) - .isAnnotationPresent(annotation); - - } catch (NoSuchFieldException e) { - if(delegate.getReadMethod() == null) return false; - answer = delegate.getReadMethod() - .isAnnotationPresent(annotation); - } - return answer; - } - - } - } -} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java index 59463ca42..f823e021a 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java @@ -19,10 +19,17 @@ import java.lang.reflect.Constructor; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Date; -import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -36,6 +43,7 @@ import javax.persistence.metamodel.PluralAttribute; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; + import graphql.language.NullValue; /** @@ -54,6 +62,10 @@ *
  • java.math.BigDecimal
  • *
  • java.lang.String
  • *
  • java.util.Date
  • + *
  • java.time.LocalDate
  • + *
  • java.time.LocalDateTime + *
  • java.time.Instant
  • + *
  • java.time.LocalTime
  • *
  • java.util.Calendar
  • *
  • java.sql.Date
  • *
  • java.sql.Time
  • @@ -63,23 +75,40 @@ */ class JpaPredicateBuilder { + public static final Map, Class> WRAPPERS_TO_PRIMITIVES = new HashMap, Class>(); + public static final Map, Class> PRIMITIVES_TO_WRAPPERS = new HashMap, Class>(); + + static { + PRIMITIVES_TO_WRAPPERS.put(boolean.class, Boolean.class); + PRIMITIVES_TO_WRAPPERS.put(byte.class, Byte.class); + PRIMITIVES_TO_WRAPPERS.put(char.class, Character.class); + PRIMITIVES_TO_WRAPPERS.put(double.class, Double.class); + PRIMITIVES_TO_WRAPPERS.put(float.class, Float.class); + PRIMITIVES_TO_WRAPPERS.put(int.class, Integer.class); + PRIMITIVES_TO_WRAPPERS.put(long.class, Long.class); + PRIMITIVES_TO_WRAPPERS.put(short.class, Short.class); + PRIMITIVES_TO_WRAPPERS.put(void.class, Void.class); + + WRAPPERS_TO_PRIMITIVES.put(Boolean.class, boolean.class); + WRAPPERS_TO_PRIMITIVES.put(Byte.class, byte.class); + WRAPPERS_TO_PRIMITIVES.put(Character.class, char.class); + WRAPPERS_TO_PRIMITIVES.put(Double.class, double.class); + WRAPPERS_TO_PRIMITIVES.put(Float.class, float.class); + WRAPPERS_TO_PRIMITIVES.put(Integer.class, int.class); + WRAPPERS_TO_PRIMITIVES.put(Long.class, long.class); + WRAPPERS_TO_PRIMITIVES.put(Short.class, short.class); + WRAPPERS_TO_PRIMITIVES.put(Void.class, void.class); + } + private final CriteriaBuilder cb; - private final EnumSet globalOptions; - /** - * Field name can be prepended with (comma separated list of local options) - * if field is prepended with (), even without options, any global options - * avoided and defaults are used.Defaults: for numbers and booleas - - * equality; for strings case insensitive beginning matches; for dates - * greater or equal; - * + * JpaPredicateBuilder constructor + * * @param cb - * @param globalOptions */ - public JpaPredicateBuilder(CriteriaBuilder cb, EnumSet globalOptions) { + public JpaPredicateBuilder(CriteriaBuilder cb) { this.cb = cb; - this.globalOptions = globalOptions; } protected Predicate addOrNull(Path root, Predicate p) { @@ -94,13 +123,12 @@ protected Predicate addOrNull(Path root, Predicate p) { * @return */ protected Predicate getStringPredicate(Path root, PredicateFilter filter) { - Expression fieldValue; - // list or arrays only for in and not in, between and not between Predicate arrayValuePredicate = mayBeArrayValuePredicate(root, filter); if(arrayValuePredicate == null) { String compareValue = filter.getValue().toString(); + Expression fieldValue = root; if (filter.getCriterias().contains(PredicateFilter.Criteria.IN)) { CriteriaBuilder.In in = cb.in(root); @@ -109,15 +137,7 @@ protected Predicate getStringPredicate(Path root, PredicateFilter filter if (filter.getCriterias().contains(PredicateFilter.Criteria.NIN)) { return cb.not(root.in(compareValue)); } - - if (filter.getCriterias().contains(PredicateFilter.Criteria.CASE)) { - fieldValue = root; - } - else { - fieldValue = cb.lower(root); - compareValue = compareValue.toLowerCase(); - } - + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { return cb.equal(fieldValue, compareValue); } @@ -319,6 +339,271 @@ protected Predicate getDatePredicate(Path root, PredicateFilter return null; } + protected Predicate getLocalDatePredicate(Path root, PredicateFilter filter) { + if (filter.getValue() != null && filter.getValue() instanceof LocalDate) { + if (filter.getCriterias().contains(PredicateFilter.Criteria.LT)) { + return cb.lessThan(root, (LocalDate) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GT)) { + return cb.greaterThan(root, (LocalDate) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GE)) { + return cb.greaterThanOrEqualTo(root, (LocalDate) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { + return cb.equal(root, filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.NE)) { + return cb.notEqual(root, filter.getValue()); + } + // LE or default + return cb.lessThanOrEqualTo(root, (LocalDate) filter.getValue()); + } else if (filter.getValue().getClass().isArray() || filter.getValue() instanceof Collection) { + if (!filter.getCriterias().contains(PredicateFilter.Criteria.NE) + && (filter.getCriterias().contains(Criteria.BETWEEN) || filter.getCriterias().contains(Criteria.NOT_BETWEEN))) { + + Object[] values; + if (filter.getValue().getClass().isArray()) { + values = (Object[]) filter.getValue(); + } else { + values = ((Collection) filter.getValue()).toArray(); + } + + if (values.length == 2) { + Expression name = (Expression) root; + Expression fromDate = cb.literal((LocalDate) values[0]); + Expression toDate = cb.literal((LocalDate) values[1]); + Predicate between = cb.between(name, fromDate, toDate); + if (filter.getCriterias().contains(Criteria.BETWEEN)) + return between; + return cb.not(between); + } + } + } + return null; + } + + protected Predicate getLocalDateTimePredicate(Path root, PredicateFilter filter) { + if (filter.getValue() != null && filter.getValue() instanceof LocalDateTime) { + if (filter.getCriterias().contains(PredicateFilter.Criteria.LT)) { + return cb.lessThan(root, (LocalDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GT)) { + return cb.greaterThan(root, (LocalDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GE)) { + return cb.greaterThanOrEqualTo(root, (LocalDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { + return cb.equal(root, filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.NE)) { + return cb.notEqual(root, filter.getValue()); + } + // LE or default + return cb.lessThanOrEqualTo(root, (LocalDateTime) filter.getValue()); + } else if (filter.getValue().getClass().isArray() || filter.getValue() instanceof Collection) { + if (!filter.getCriterias().contains(PredicateFilter.Criteria.NE) + && (filter.getCriterias().contains(Criteria.BETWEEN) || filter.getCriterias().contains(Criteria.NOT_BETWEEN))) { + + Object[] values; + if (filter.getValue().getClass().isArray()) { + values = (Object[]) filter.getValue(); + } else { + values = ((Collection) filter.getValue()).toArray(); + } + + if (values.length == 2) { + Expression name = (Expression) root; + Expression fromDateTime = cb.literal((LocalDateTime) values[0]); + Expression toDateTime = cb.literal((LocalDateTime) values[1]); + Predicate between = cb.between(name, fromDateTime, toDateTime); + if (filter.getCriterias().contains(Criteria.BETWEEN)) + return between; + return cb.not(between); + } + } + } + return null; + } + + protected Predicate getInstantPredicate(Path root, PredicateFilter filter) { + if (filter.getValue() != null && filter.getValue() instanceof Instant) { + if (filter.getCriterias().contains(PredicateFilter.Criteria.LT)) { + return cb.lessThan(root, (Instant) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GT)) { + return cb.greaterThan(root, (Instant) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GE)) { + return cb.greaterThanOrEqualTo(root, (Instant) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { + return cb.equal(root, filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.NE)) { + return cb.notEqual(root, filter.getValue()); + } + // LE or default + return cb.lessThanOrEqualTo(root, (Instant) filter.getValue()); + } else if (filter.getValue().getClass().isArray() || filter.getValue() instanceof Collection) { + if (!filter.getCriterias().contains(PredicateFilter.Criteria.NE) + && (filter.getCriterias().contains(Criteria.BETWEEN) || filter.getCriterias().contains(Criteria.NOT_BETWEEN))) { + + Object[] values; + if (filter.getValue().getClass().isArray()) { + values = (Object[]) filter.getValue(); + } else { + values = ((Collection) filter.getValue()).toArray(); + } + + if (values.length == 2) { + Expression name = (Expression) root; + Expression fromDate = cb.literal((Instant) values[0]); + Expression toDate = cb.literal((Instant) values[1]); + Predicate between = cb.between(name, fromDate, toDate); + if (filter.getCriterias().contains(Criteria.BETWEEN)) + return between; + return cb.not(between); + } + } + } + return null; + } + + protected Predicate getLocalTimePredicate(Path root, PredicateFilter filter) { + if (filter.getValue() != null && filter.getValue() instanceof LocalTime) { + if (filter.getCriterias().contains(PredicateFilter.Criteria.LT)) { + return cb.lessThan(root, (LocalTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GT)) { + return cb.greaterThan(root, (LocalTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GE)) { + return cb.greaterThanOrEqualTo(root, (LocalTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { + return cb.equal(root, filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.NE)) { + return cb.notEqual(root, filter.getValue()); + } + // LE or default + return cb.lessThanOrEqualTo(root, (LocalTime) filter.getValue()); + } else if (filter.getValue().getClass().isArray() || filter.getValue() instanceof Collection) { + if (!filter.getCriterias().contains(PredicateFilter.Criteria.NE) + && (filter.getCriterias().contains(Criteria.BETWEEN) || filter.getCriterias().contains(Criteria.NOT_BETWEEN))) { + + Object[] values; + if (filter.getValue().getClass().isArray()) { + values = (Object[]) filter.getValue(); + } else { + values = ((Collection) filter.getValue()).toArray(); + } + + if (values.length == 2) { + Expression name = (Expression) root; + Expression fromTime = cb.literal((LocalTime) values[0]); + Expression toTime = cb.literal((LocalTime) values[1]); + Predicate between = cb.between(name, fromTime, toTime); + if (filter.getCriterias().contains(Criteria.BETWEEN)) + return between; + return cb.not(between); + } + } + } + return null; + } + + protected Predicate getZonedDateTimePredicate(Path root, PredicateFilter filter) { + if (filter.getValue() != null && filter.getValue() instanceof ZonedDateTime) { + if (filter.getCriterias().contains(PredicateFilter.Criteria.LT)) { + return cb.lessThan(root, (ZonedDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GT)) { + return cb.greaterThan(root, (ZonedDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GE)) { + return cb.greaterThanOrEqualTo(root, (ZonedDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { + return cb.equal(root, filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.NE)) { + return cb.notEqual(root, filter.getValue()); + } + // LE or default + return cb.lessThanOrEqualTo(root, (ZonedDateTime) filter.getValue()); + } else if (filter.getValue().getClass().isArray() || filter.getValue() instanceof Collection) { + if (!filter.getCriterias().contains(PredicateFilter.Criteria.NE) + && (filter.getCriterias().contains(Criteria.BETWEEN) || filter.getCriterias().contains(Criteria.NOT_BETWEEN))) { + + Object[] values; + if (filter.getValue().getClass().isArray()) { + values = (Object[]) filter.getValue(); + } else { + values = ((Collection) filter.getValue()).toArray(); + } + + if (values.length == 2) { + Expression name = (Expression) root; + Expression fromDateTime = cb.literal((ZonedDateTime) values[0]); + Expression toDateTime = cb.literal((ZonedDateTime) values[1]); + Predicate between = cb.between(name, fromDateTime, toDateTime); + if (filter.getCriterias().contains(Criteria.BETWEEN)) + return between; + return cb.not(between); + } + } + } + return null; + } + + protected Predicate getOffsetDateTimePredicate(Path root, PredicateFilter filter) { + if (filter.getValue() != null && filter.getValue() instanceof OffsetDateTime) { + if (filter.getCriterias().contains(PredicateFilter.Criteria.LT)) { + return cb.lessThan(root, (OffsetDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GT)) { + return cb.greaterThan(root, (OffsetDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.GE)) { + return cb.greaterThanOrEqualTo(root, (OffsetDateTime) filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.EQ)) { + return cb.equal(root, filter.getValue()); + } + if (filter.getCriterias().contains(PredicateFilter.Criteria.NE)) { + return cb.notEqual(root, filter.getValue()); + } + // LE or default + return cb.lessThanOrEqualTo(root, (OffsetDateTime) filter.getValue()); + } else if (filter.getValue().getClass().isArray() || filter.getValue() instanceof Collection) { + if (!filter.getCriterias().contains(PredicateFilter.Criteria.NE) + && (filter.getCriterias().contains(Criteria.BETWEEN) || filter.getCriterias().contains(Criteria.NOT_BETWEEN))) { + + Object[] values; + if (filter.getValue().getClass().isArray()) { + values = (Object[]) filter.getValue(); + } else { + values = ((Collection) filter.getValue()).toArray(); + } + + if (values.length == 2) { + Expression name = (Expression) root; + Expression fromDateTime = cb.literal((OffsetDateTime) values[0]); + Expression toDateTime = cb.literal((OffsetDateTime) values[1]); + Predicate between = cb.between(name, fromDateTime, toDateTime); + if (filter.getCriterias().contains(Criteria.BETWEEN)) + return between; + return cb.not(between); + } + } + } + return null; + } + + private Predicate getUuidPredicate(Path field, PredicateFilter filter) { if (filter.getValue() == null) { return null; @@ -389,7 +674,7 @@ private Predicate getTypedPredicate(From from, Path field, PredicateFilt PredicateFilter predicateFilter = new PredicateFilter(filter.getField(), value, criterias); if (type.isPrimitive()) - type = JpaQueryBuilder.PRIMITIVES_TO_WRAPPERS.get(type); + type = PRIMITIVES_TO_WRAPPERS.get(type); if (type.equals(String.class)) { return getStringPredicate((Path)field, filter); } @@ -408,6 +693,24 @@ else if (type.equals(BigDecimal.class) else if (type.equals(java.util.Date.class)) { return getDatePredicate((Path) field, predicateFilter); } + else if(type.equals(java.time.LocalDate.class)){ + return getLocalDatePredicate((Path) field, predicateFilter); + } + else if(type.equals(LocalDateTime.class)){ + return getLocalDateTimePredicate((Path) field, predicateFilter); + } + else if(type.equals(Instant.class)){ + return getInstantPredicate((Path) field, predicateFilter); + } + else if(type.equals(LocalTime.class)){ + return getLocalTimePredicate((Path) field, predicateFilter); + } + else if(type.equals(ZonedDateTime.class)){ + return getZonedDateTimePredicate((Path) field, predicateFilter); + } + else if(type.equals(OffsetDateTime.class)){ + return getOffsetDateTimePredicate((Path) field, predicateFilter); + } else if (type.equals(Boolean.class)) { return getBooleanPredicate(field, predicateFilter); } @@ -495,8 +798,7 @@ private Object getValue(Object object, Class type) { Object arg = NullValue.class.isInstance(object) ? null : object; return constructor.newInstance(arg); } - } catch (Exception e) { - e.printStackTrace(); + } catch (Exception ignored) { } return object; diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaQueryBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaQueryBuilder.java deleted file mode 100644 index 0279b07e5..000000000 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaQueryBuilder.java +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright 2017 IntroPro Ventures Inc. and/or its affiliates. - * - * 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 - * limitations under the License. - */ - -package com.introproventures.graphql.jpa.query.schema.impl; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import javax.persistence.EntityManager; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; -import javax.persistence.criteria.Order; -import javax.persistence.criteria.Path; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; -import javax.persistence.metamodel.Attribute; -import javax.persistence.metamodel.EntityType; -import javax.persistence.metamodel.Metamodel; -import javax.persistence.metamodel.PluralAttribute; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.introproventures.graphql.jpa.query.annotation.GraphQLDefaultOrderBy; - -/** - * Jpa Criteria Query Builder class used to apply predicate filters and orders and build Criteria Query. - * - * @param - * @todo make inner joins for compound fields garanteed to have joined data - */ -class JpaQueryBuilder { - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - private final Class clazz; - - private final CriteriaBuilder criteriaBuilder; - - private final JpaPredicateBuilder predicateBuilder; - - private final EnumSet options; - - private final Metamodel metamodel; - - private final List filters = new ArrayList<>(); - - private final LinkedHashMap orders = new LinkedHashMap<>(); - - public static final Map, Class> WRAPPERS_TO_PRIMITIVES = new HashMap, Class>(); - public static final Map, Class> PRIMITIVES_TO_WRAPPERS = new HashMap, Class>(); - - static { - PRIMITIVES_TO_WRAPPERS.put(boolean.class, Boolean.class); - PRIMITIVES_TO_WRAPPERS.put(byte.class, Byte.class); - PRIMITIVES_TO_WRAPPERS.put(char.class, Character.class); - PRIMITIVES_TO_WRAPPERS.put(double.class, Double.class); - PRIMITIVES_TO_WRAPPERS.put(float.class, Float.class); - PRIMITIVES_TO_WRAPPERS.put(int.class, Integer.class); - PRIMITIVES_TO_WRAPPERS.put(long.class, Long.class); - PRIMITIVES_TO_WRAPPERS.put(short.class, Short.class); - PRIMITIVES_TO_WRAPPERS.put(void.class, Void.class); - - WRAPPERS_TO_PRIMITIVES.put(Boolean.class, boolean.class); - WRAPPERS_TO_PRIMITIVES.put(Byte.class, byte.class); - WRAPPERS_TO_PRIMITIVES.put(Character.class, char.class); - WRAPPERS_TO_PRIMITIVES.put(Double.class, double.class); - WRAPPERS_TO_PRIMITIVES.put(Float.class, float.class); - WRAPPERS_TO_PRIMITIVES.put(Integer.class, int.class); - WRAPPERS_TO_PRIMITIVES.put(Long.class, long.class); - WRAPPERS_TO_PRIMITIVES.put(Short.class, short.class); - WRAPPERS_TO_PRIMITIVES.put(Void.class, void.class); - } - - /** - * Creates new instance from JPA entity class. - * - * @param em - * @param clazz must be JPA Entity annotated - */ - public JpaQueryBuilder(EntityManager em, Class clazz) { - this.clazz = clazz; - this.criteriaBuilder = em.getCriteriaBuilder(); - //this.query = cb.createQuery(clazz); - //this.root = query.from(clazz); - this.options = EnumSet.noneOf(Logical.class); - this.predicateBuilder = new JpaPredicateBuilder(criteriaBuilder, options); - this.metamodel = em.getMetamodel(); - } - - /** - * Creates new instance from existing query - * - * @param em - * @param query - */ - public JpaQueryBuilder(EntityManager em, CriteriaQuery query) { - this.criteriaBuilder = em.getCriteriaBuilder(); - this.clazz = query.getResultType(); - //this.query = query; - this.options = EnumSet.noneOf(Logical.class); - this.predicateBuilder = new JpaPredicateBuilder(criteriaBuilder, options); - this.metamodel = em.getMetamodel(); - } - - /** - * Checks if filter fieldname is valid for given query. Throws verbose - * IllegalArgumentException if not. If checkValue = true, checks if filter - * value is of type of the field or can be converted. This is to avoid - * unnecessarry joins if filter is invalid - * - * @param filter - * @param checkValue if false than value is not checked - * @return true if filter is valid - * @throws IllegalArgumentException - */ - private boolean checkFilterValid(PredicateFilter filter, boolean checkValue) { - Class fieldType = getJavaType(filter.getField()); - // arrays - if (filter.getValue().getClass().isArray()) { - Object[] arr = (Object[]) filter.getValue(); - if (arr.length == 0) { - return false; - } else { - return !checkValue || ((Object[]) filter.getValue())[0].getClass().equals(fieldType); - } - } - if (fieldType.isPrimitive()) { - return !checkValue || PRIMITIVES_TO_WRAPPERS.get(fieldType).isInstance(filter.getValue()); - } else { - return !checkValue || fieldType.isInstance(filter.getValue()); - } - } - - /** - * This clumsy code is just to get the class of plural attribute mapping - * - * @param et - * @param fieldName - * @return - */ - private Class getPluralJavaType(EntityType et, String fieldName) { - for (PluralAttribute pa : et.getPluralAttributes()) { - if (pa.getName().equals(fieldName)) { - switch (pa.getCollectionType()) { - case COLLECTION: - return et.getCollection(fieldName).getElementType().getJavaType(); - case LIST: - return et.getList(fieldName).getElementType().getJavaType(); - case SET: - return et.getSet(fieldName).getElementType().getJavaType(); - case MAP: - throw new UnsupportedOperationException("Entity Map mapping unsupported for entity: " + et.getName() + " field name: " + fieldName); - } - } - } - throw new IllegalArgumentException("Field " + fieldName + " of entity " + et.getName() + " is not a plural attribute"); - } - - /** - * Returns Java type of the fieldName - * - * @param fieldName - * @return - * @throws IllegalArgumentException if fieldName isn't valid for given - * entity - */ - public Class getJavaType(String fieldName) { - - String[] compoundField = fieldName.split("\\."); - EntityType et = metamodel.entity(clazz); - - for (int i = 0; i < compoundField.length; i++) { - if (i < (compoundField.length - 1)) { - try { - Attribute att = et.getAttribute(compoundField[i]); - if (att.isCollection()) { - et = metamodel.entity(getPluralJavaType(et, compoundField[i])); - } else { - et = metamodel.entity(et.getAttribute(compoundField[i]).getJavaType()); - } - } catch (IllegalArgumentException | IllegalStateException e) { - throw new IllegalArgumentException( - "Illegal field name " + fieldName + " (" + compoundField[i] + ") for root type " + clazz - ); - } - } else { - try { - return et.getAttribute(compoundField[i]).getJavaType(); - } catch (IllegalArgumentException | IllegalStateException e) { - throw new IllegalArgumentException( - "Illegal field name " + fieldName + " (" + compoundField[i] + ") for root type " + clazz - ); - } - } - } - return null; // should never be reached - } - - /** - * Adds filters to the query. Preserves existing filters. - * - * @param conditions - * @return this, itself for chain calls - */ - public JpaQueryBuilder addFilters(List conditions) { - if (conditions != null) { - for (PredicateFilter f : conditions) { - if (checkFilterValid(f, true)) { - filters.add(f); - } else { - logger.error("Could not apply filter for field: " + f.getField() + " value: " + f.getValue() + " of type: " + f.getValue().getClass()); - } - } - } - - // keep it sorted to avoid inner/outer joins messup - Collections.sort(filters); - - return this; - } - - /** - * Adds filter to the query. Preserves existing filters. - * - * @param conditions - * @return this, itself for chain calls - */ - public JpaQueryBuilder addFilter(PredicateFilter filter) { - if (filter != null) { - if (checkFilterValid(filter, true)) { - filters.add(filter); - } else { - logger.error("Could not apply filter for field: " + filter.getField() + " value: " + filter.getValue() + " of type: " + filter.getValue().getClass()); - } - } - - // keep it sorted to avoid inner/outer joins messup - Collections.sort(filters); - - return this; - } - - /** - * @param from - * @param query - */ - private void applyFilters(Root from, CriteriaQuery query) { - - List predicates = new ArrayList<>(); - - for (PredicateFilter filter : filters) { - Path path; - if (filter.getCriterias().contains(PredicateFilter.Criteria.IS_NULL)) { - path = getCompoundJoinedPath(from, filter.getField(), true); - } else { - path = getCompoundJoinedPath(from, filter.getField(), false); - } - - Predicate p = predicateBuilder.getPredicate(from, path, filter); - if (p != null) { - predicates.add(p); - } - } - // this does not work for Hibernate!!! - if (query.getRestriction() != null) { - predicates.add(query.getRestriction()); - } - if (options.contains(Logical.OR)) { - query.where(criteriaBuilder.or(predicates.toArray(new Predicate[0]))); - } else { - query.where(criteriaBuilder.and(predicates.toArray(new Predicate[0]))); - } - - } - - private void applyOrders(Root from, CriteriaQuery query) { - List orderList = new ArrayList<>(); - - for (Map.Entry me : orders.entrySet()) { - Path path = getCompoundJoinedPath(from, me.getKey(), true); - if (me.getValue() == null || me.getValue().equals(true)) { - orderList.add(criteriaBuilder.asc(path)); - } else { - orderList.add(criteriaBuilder.desc(path)); - } - } - query.orderBy(orderList); - } - - /** - * Adds order by expressions to the tail of already existing orders of query - * - * @param orders - * @return - */ - public JpaQueryBuilder addOrders(Map orders) { - - for (Map.Entry me : orders.entrySet()) { - checkFilterValid(new PredicateFilter(me.getKey(), "", EnumSet.noneOf(PredicateFilter.Criteria.class)), false); - this.orders.put(me.getKey(), me.getValue()); - } - return this; - } - - /** - * Adds order by expressions to the tail of already existing orders of query - * - * @param orders - * @return - */ - public JpaQueryBuilder addOrder(String field, Boolean direction) { - - checkFilterValid(new PredicateFilter(field, "", EnumSet.noneOf(PredicateFilter.Criteria.class)), false); - this.orders.put(field, direction); - - return this; - } - - /** - * @param fieldName - * @return Path of compound field to the primitive type - */ - private Path getCompoundJoinedPath(Root rootPath, String fieldName, boolean outer) { - String[] compoundField = fieldName.split("\\."); - - Join join; - - if (compoundField.length == 1) { - return rootPath.get(compoundField[0]); - } else { - join = reuseJoin(rootPath, compoundField[0], outer); - } - - for (int i = 1; i < compoundField.length; i++) { - if (i < (compoundField.length - 1)) { - join = reuseJoin(join, compoundField[i], outer); - } else { - return join.get(compoundField[i]); - } - } - - return null; - } - - // trying to find already existing joins to reuse - private Join reuseJoin(From path, String fieldName, boolean outer) { - for (Join join : path.getJoins()) { - if (join.getAttribute().getName().equals(fieldName)) { - if ((join.getJoinType() == JoinType.LEFT) == outer) { - logger.debug("Reusing existing join for field " + fieldName); - return join; - } - } - } - return outer ? path.join(fieldName, JoinType.LEFT) : path.join(fieldName); - } - - - /** - * Get sorting field - * - * @param clazz - * @return - * @throws IllegalArgumentException - * @throws IllegalAccessException - */ - private Field getSortAnnotation(Class clazz) throws IllegalArgumentException, IllegalAccessException { - for (Field f : clazz.getDeclaredFields()) { - if (f.getAnnotation(GraphQLDefaultOrderBy.class) != null) { - return f; - } - } - //if not found, search in superclass. todo recursive search - for (Field f : clazz.getSuperclass().getDeclaredFields()) { - if (f.getAnnotation(GraphQLDefaultOrderBy.class) != null) { - return f; - } - } - return null; - } - - - /** - * Resulting query with filters and orders, if orders are empty, than makes - * default ascending ordering by root id to prevent paging confuses - * - * @return - */ - public CriteriaQuery getQuery() { - CriteriaQuery query = criteriaBuilder.createQuery(clazz); - Root from = query.from(clazz); - applyFilters(from, query); - applyOrders(from, query); - - // add default ordering - if (query.getOrderList() == null || query.getOrderList().isEmpty()) { - EntityType entityType = from.getModel(); - try { - Field sortField = getSortAnnotation(entityType.getBindableJavaType()); - if (sortField == null) - query.orderBy(criteriaBuilder.asc(from.get(entityType.getId(entityType.getIdType().getJavaType()).getName()))); - else { - GraphQLDefaultOrderBy order = sortField.getAnnotation(GraphQLDefaultOrderBy.class); - if (order.asc()) { - query.orderBy(criteriaBuilder.asc(from.get(sortField.getName()))); - } else { - query.orderBy(criteriaBuilder.desc(from.get(sortField.getName()))); - } - - } - } catch (Exception ex) { - logger.warn("In" + this.getClass().getName(), ex); - } - } - return query; - } - - /** - * @return - */ - public CriteriaQuery getCountQuery() { - CriteriaQuery q = criteriaBuilder.createQuery(Long.class); - Root root = q.from(clazz); - q.select(criteriaBuilder.count(root)); - applyFilters(root, q); - return q; - } - - public EnumSet getOptions() { - return options; - } - -} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java index 06ea2778c..dd42d963c 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java @@ -19,6 +19,11 @@ import static graphql.introspection.Introspection.TypeMetaFieldDef; import static graphql.introspection.Introspection.TypeNameMetaFieldDef; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +35,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -57,10 +63,12 @@ import com.introproventures.graphql.jpa.query.annotation.GraphQLDefaultOrderBy; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; + import graphql.GraphQLException; import graphql.execution.ValuesResolver; import graphql.language.Argument; import graphql.language.ArrayValue; +import graphql.language.AstValueHelper; import graphql.language.BooleanValue; import graphql.language.Comment; import graphql.language.EnumValue; @@ -79,6 +87,7 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentBuilder; +import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; @@ -98,8 +107,6 @@ class QraphQLJpaBaseDataFetcher implements DataFetcher { private static final String WHERE = "where"; protected static final String OPTIONAL = "optional"; - - protected static final List ARGUMENTS = Arrays.asList(OPTIONAL); // "__typename" is part of the graphql introspection spec and has to be ignored private static final String TYPENAME = "__typename"; @@ -150,7 +157,7 @@ protected TypedQuery getQuery(DataFetchingEnvironment environment, Field fiel return entityManager.createQuery(query.distinct(isDistinct)); } - + protected final List getFieldPredicates(Field field, CriteriaQuery query, CriteriaBuilder cb, Root root, From from, DataFetchingEnvironment environment) { List arguments = new ArrayList<>(); @@ -160,9 +167,9 @@ protected final List getFieldPredicates(Field field, CriteriaQuery field.getSelectionSet().getSelections().forEach(selection -> { if (selection instanceof Field) { Field selectedField = (Field) selection; - + // "__typename" is part of the graphql introspection spec and has to be ignored by jpa - if(!TYPENAME.equals(selectedField.getName()) && !IntrospectionUtils.isTransient(from.getJavaType(), selectedField.getName())) { + if(isPersistent(environment, selectedField.getName())) { Path fieldPath = from.get(selectedField.getName()); From fetch = null; @@ -173,16 +180,17 @@ protected final List getFieldPredicates(Field field, CriteriaQuery // Build predicate arguments for singular attributes only if(fieldPath.getModel() instanceof SingularAttribute) { // Process the orderBy clause - Optional orderByArgument = selectedField.getArguments().stream() - .filter(this::isOrderByArgument) - .findFirst(); - - if (orderByArgument.isPresent()) { - if ("DESC".equals(((EnumValue) orderByArgument.get().getValue()).getName())) - query.orderBy(cb.desc(fieldPath)); - else - query.orderBy(cb.asc(fieldPath)); - } + selectedField.getArguments().stream() + .filter(this::isOrderByArgument) + .findFirst() + .map(a -> getOrderByValue(a, environment)) + .ifPresent(orderBy -> { + if ("DESC".equals(orderBy.getName())) { + query.orderBy(cb.desc(fieldPath)); + } else { + query.orderBy(cb.asc(fieldPath)); + } + }); // Check if it's an object and the foreign side is One. Then we can eagerly join causing an inner join instead of 2 queries SingularAttribute attribute = (SingularAttribute) fieldPath.getModel(); @@ -210,20 +218,20 @@ protected final List getFieldPredicates(Field field, CriteriaQuery // Let's do fugly conversion // the many end is a collection, and it is always optional by default (empty collection) isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) - .orElse(toManyDefaultOptional); - - // Let's apply join to retrieve associated collection - fetch = reuseFetch(from, selectedField.getName(), isOptional); + .orElse(toManyDefaultOptional); - // Let's fetch element collections to avoid filtering their values used where search criteria GraphQLObjectType objectType = getObjectType(environment); EntityType entityType = getEntityType(objectType); PluralAttribute attribute = (PluralAttribute) entityType.getAttribute(selectedField.getName()); + // Let's join fetch element collections to avoid filtering their values used where search criteria if(PersistentAttributeType.ELEMENT_COLLECTION == attribute.getPersistentAttributeType()) { - from.fetch(selectedField.getName()); - } + from.fetch(selectedField.getName(), JoinType.LEFT); + } else { + // Let's apply fetch join to retrieve associated plural attributes + fetch = reuseFetch(from, selectedField.getName(), isOptional); + } } // Let's build join fetch graph to avoid Hibernate error: // "query specified join fetching, but the owner of the fetched association was not present in the select list" @@ -378,12 +386,40 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root from, From pat @SuppressWarnings( "unchecked" ) - private R getValue(Argument argument) { - return (R) argument.getValue(); + private > R getValue(Argument argument, DataFetchingEnvironment environment) { + Value value = argument.getValue(); + + if(VariableReference.class.isInstance(value)) { + Object variableValue = getVariableReferenceValue((VariableReference) value, environment); + + GraphQLArgument graphQLArgument = environment.getExecutionStepInfo() + .getFieldDefinition() + .getArgument(argument.getName()); + + return (R) AstValueHelper.astFromValue(variableValue, graphQLArgument.getType()); + } + + return (R) value; + } + + private EnumValue getOrderByValue(Argument argument, DataFetchingEnvironment environment) { + Value value = argument.getValue(); + + if(VariableReference.class.isInstance(value)) { + Object variableValue = getVariableReferenceValue((VariableReference) value, environment); + return EnumValue.newEnumValue(variableValue.toString()).build(); + } + return (EnumValue) value; + } + + private Object getVariableReferenceValue(VariableReference variableReference, DataFetchingEnvironment env) { + return env.getExecutionContext() + .getVariables() + .get(variableReference.getName()); } protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From path, DataFetchingEnvironment environment, Argument argument) { - ObjectValue whereValue = getValue(argument); + ObjectValue whereValue = getValue(argument, environment); if(whereValue.getChildren().isEmpty()) return cb.conjunction(); @@ -404,7 +440,7 @@ protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From from, DataFetchingEnvironment environment, Argument argument) { - ObjectValue whereValue = getValue(argument); + ObjectValue whereValue = getValue(argument, environment); if (whereValue.getChildren().isEmpty()) return cb.disjunction(); @@ -494,7 +530,7 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, From path, DataFetchingEnvironment environment, Argument argument) { - ArrayValue whereValue = getValue(argument); + ArrayValue whereValue = getValue(argument, environment); if (whereValue.getValues().isEmpty()) return cb.disjunction(); @@ -717,7 +753,7 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From } // Let's parse simple Criteria expressions, i.e. EQ, LIKE, etc. - JpaPredicateBuilder pb = new JpaPredicateBuilder(cb, EnumSet.of(Logical.AND)); + JpaPredicateBuilder pb = new JpaPredicateBuilder(cb); expressionValue.getObjectFields() .stream() @@ -896,22 +932,41 @@ else if (value instanceof VariableReference) { return argumentValue; } } else if (value instanceof ArrayValue) { - Object convertedValue = environment.getArgument(argument.getName()); - if (convertedValue != null && !getJavaType(environment, argument).isEnum()) { - // unwrap [[EnumValue{name='value'}]] - if(convertedValue instanceof Collection - && ((Collection) convertedValue).stream().allMatch(it->it instanceof Collection)) { - convertedValue = ((Collection) convertedValue).iterator().next(); + Collection arrayValue = environment.getArgument(argument.getName()); + + if (arrayValue != null) { + // Let's unwrap array of array values + if(arrayValue.stream() + .allMatch(it->it instanceof Collection)) { + arrayValue = Collection.class.cast(arrayValue.iterator() + .next()); } - - if(convertedValue instanceof Collection - && ((Collection) convertedValue).stream().anyMatch(it->it instanceof Value)) { - return ((Collection) convertedValue).stream() - .map((it) -> convertValue(environment, argument, (Value) it)) - .collect(Collectors.toList()); + + // Let's convert enum types, i.e. array of strings or EnumValue into Java type + if(getJavaType(environment, argument).isEnum()) { + Function objectValue = (obj) -> Value.class.isInstance(obj) + ? Value.class.cast(obj) + : new EnumValue(obj.toString()); + // Return real typed resolved array values converted into Java enums + return arrayValue.stream() + .map((it) -> convertValue(environment, + argument, + objectValue.apply(it))) + .collect(Collectors.toList()); + } + // Let's try handle Ast Value types + else if(arrayValue.stream() + .anyMatch(it->it instanceof Value)) { + return arrayValue.stream() + .map(it -> convertValue(environment, + argument, + Value.class.cast(it))) + .collect(Collectors.toList()); + } + // Return real typed resolved array value, i.e. Date, UUID, Long + else { + return arrayValue; } - // Return real typed resolved array value - return convertedValue; } else { // Wrap converted values in ArrayList return ((ArrayValue) value).getValues().stream() @@ -929,11 +984,55 @@ else if (value instanceof EnumValue) { return ((BooleanValue) value).isValue(); } else if (value instanceof FloatValue) { return ((FloatValue) value).getValue(); + } else if (value instanceof ObjectValue) { + Class javaType = getJavaType(environment, argument); + Map values = environment.getArgument(argument.getName()); + + try { + return getJavaBeanValue(javaType, values); + } catch (Exception cause) { + throw new RuntimeException(cause); + } } - //return value.toString(); return value; } + + private Object getJavaBeanValue(Class javaType, Map values) throws Exception { + Constructor constructor = javaType.getConstructor(); + constructor.setAccessible(true); + + Object javaBean = constructor.newInstance(); + + values.entrySet() + .stream() + .forEach(entry -> { + setPropertyValue(javaBean, + entry.getKey(), + entry.getValue()); + }); + + return javaBean; + } + + private void setPropertyValue(Object javaBean, String propertyName, Object propertyValue) { + try { + BeanInfo bi = Introspector.getBeanInfo(javaBean.getClass()); + PropertyDescriptor pds[] = bi.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + if (pd.getName().equals(propertyName)) { + Method setter = pd.getWriteMethod(); + setter.setAccessible(true); + + if (setter != null) { + setter.invoke(javaBean, new Object[] {propertyValue} ); + } + } + } + } catch (Exception ignored) { + // ignore + } + } /** * Resolve Java type from associated query argument JPA model attribute @@ -1076,7 +1175,7 @@ && isManagedType(entityType.getAttribute(it.getName())) Subgraph sg = entityGraph.addSubgraph(it.getName()); buildSubgraph(it, sg); } else { - if(!TYPENAME.equals(it.getName()) && !IntrospectionUtils.isTransient(entityType.getJavaType(), it.getName())) + if(isPersistent(entityType, it.getName())) entityGraph.addAttributeNodes(it.getName()); } }); @@ -1175,6 +1274,34 @@ protected final Optional getSelectionField(Field field, String fieldName) .filter(it -> fieldName.equals(it.getName())) .findFirst(); } + + protected boolean isPersistent(DataFetchingEnvironment environment, + String attributeName) { + GraphQLObjectType objectType = getObjectType(environment); + EntityType entityType = getEntityType(objectType); + + return isPersistent(entityType, attributeName); + } + + protected boolean isPersistent(EntityType entityType, + String attributeName) { + try { + return entityType.getAttribute(attributeName) != null; + } catch (Exception ignored) { } + + return false; + } + + protected boolean isTransient(DataFetchingEnvironment environment, + String attributeName) { + return !isPersistent(environment, attributeName); + } + + protected boolean isTransient(EntityType entityType, + String attributeName) { + return !isPersistent(entityType, attributeName); + } + @SuppressWarnings("rawtypes") class NullValue implements Value { diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaConverterTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaConverterTests.java index 8d2d26ea6..e22e0112d 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaConverterTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/converter/GraphQLJpaConverterTests.java @@ -786,7 +786,7 @@ public void queryTasksVariablesWhereWithExplicitANDEXISTSByNameAndValueCriteria( // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryTasksVariablesWhereWithEXISTSByNameAndValueCriteria() { @@ -829,6 +829,6 @@ public void queryTasksVariablesWhereWithEXISTSByNameAndValueCriteria() { // then assertThat(result.toString()).isEqualTo(expected); } - - + + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/EntityWithEmbeddedIdTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/EntityWithEmbeddedIdTest.java new file mode 100644 index 000000000..a801e5091 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/EntityWithEmbeddedIdTest.java @@ -0,0 +1,138 @@ +package com.introproventures.graphql.jpa.query.embeddedid; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.persistence.EntityManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, + properties = "spring.datasource.data=EntityWithEmbeddedIdTest.sql") +@TestPropertySource({"classpath:hibernate.properties"}) +public class EntityWithEmbeddedIdTest { + + @SpringBootApplication + static class Application { + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + return new GraphQLJpaSchemaBuilder(entityManager) + .name("EntityWithEmbeddedIdTest"); + } + } + + @Autowired + private GraphQLExecutor executor; + + @Test + public void queryBookWithEmbeddedId() { + //given + String query = "query {" + + " Book(" + + " bookId: {" + + " title: \"War and Piece\"" + + " language: \"Russian\"" + + " }" + + " )" + + " {" + + " bookId {" + + " title" + + " language" + + " }" + + " description" + + " }" + + "}"; + + String expected = "{Book={bookId={title=War and Piece, language=Russian}, description=War and Piece Novel}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksWithyWithEmbeddedId() { + //given + String query = "query {" + + " Books {" + + " total" + + " pages" + + " select {" + + " bookId {" + + " title" + + " language" + + " }" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{Books={total=2, pages=1, select=[" + + "{bookId={title=Witch Of Water, language=English}, description=Witch Of Water Fantasy}, " + + "{bookId={title=War and Piece, language=Russian}, description=War and Piece Novel}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksWithyWithEmbeddedIdWhereCriteriaExpression() { + //given + String query = "query {" + + " Books( " + + " where: {" + + " bookId: {" + + " EQ: {" + + " title: \"War and Piece\"" + + " language: \"Russian\"" + + " }" + + " }" + + " }" + + " ){" + + " total" + + " pages" + + " select {" + + " bookId {" + + " title" + + " language" + + " }" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{Books={total=1, pages=1, select=[" + + "{bookId={title=War and Piece, language=Russian}, description=War and Piece Novel}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/model/Book.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/model/Book.java new file mode 100644 index 000000000..c7a4812f1 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/model/Book.java @@ -0,0 +1,17 @@ +package com.introproventures.graphql.jpa.query.embeddedid.model; + + +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; + +import lombok.Data; + +@Data +@Entity +public class Book { + + @EmbeddedId + private BookId bookId; + + private String description; +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/model/BookId.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/model/BookId.java new file mode 100644 index 000000000..e01144d40 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/embeddedid/model/BookId.java @@ -0,0 +1,17 @@ +package com.introproventures.graphql.jpa.query.embeddedid.model; + + +import java.io.Serializable; + +import javax.persistence.Embeddable; + +import lombok.Data; + +@Data +@Embeddable +public class BookId implements Serializable { + private static final long serialVersionUID = 1L; + + private String title; + private String language; +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/EntityWithIdClassTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/EntityWithIdClassTest.java new file mode 100644 index 000000000..a4d09b8cb --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/EntityWithIdClassTest.java @@ -0,0 +1,130 @@ +package com.introproventures.graphql.jpa.query.idclass; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.persistence.EntityManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, + properties = "spring.datasource.data=EntityWithIdClassTest.sql") +@TestPropertySource({"classpath:hibernate.properties"}) +public class EntityWithIdClassTest { + + @SpringBootApplication + static class Application { + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + return new GraphQLJpaSchemaBuilder(entityManager) + .name("IdClassCompsiteKeysTest"); + } + } + + @Autowired + private GraphQLExecutor executor; + + @Test + public void querySingularEntityWithIdClass() { + //given + String query = "query {" + + " Account(" + + " accountNumber: \"1\"" + + " accountType: \"Savings\"" + + " )" + + " {" + + " accountNumber" + + " accountType" + + " description" + + " }" + + "}"; + + String expected = "{Account={accountNumber=1, accountType=Savings, description=Saving account record}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryEntityWithIdClass() { + //given + String query = "query {" + + " Accounts {" + + " total" + + " pages" + + " select {" + + " accountNumber" + + " accountType" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{Accounts={total=2, pages=1, select=[" + + "{accountNumber=1, accountType=Savings, description=Saving account record}, " + + "{accountNumber=2, accountType=Checking, description=Checking account record}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryEntityWithIdClassWhereCriteriaExpression() { + //given + String query = "query {" + + " Accounts(" + + " where: {" + + " accountNumber: {" + + " EQ: \"1\"" + + " }" + + " accountType: {" + + " EQ: \"Savings\"" + + " }" + + " })" + + " {" + + " total" + + " pages" + + " select {" + + " accountNumber" + + " accountType" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{Accounts={total=1, pages=1, select=[" + + "{accountNumber=1, accountType=Savings, description=Saving account record}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/model/Account.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/model/Account.java new file mode 100644 index 000000000..ca3c477bf --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/model/Account.java @@ -0,0 +1,22 @@ +package com.introproventures.graphql.jpa.query.idclass.model; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; + +import lombok.Data; + +@Entity +@IdClass(AccountId.class) +@Data +public class Account { + + @Id + private String accountNumber; + + @Id + private String accountType; + + private String description; + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/model/AccountId.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/model/AccountId.java new file mode 100644 index 000000000..afc3f8b9a --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/idclass/model/AccountId.java @@ -0,0 +1,15 @@ +package com.introproventures.graphql.jpa.query.idclass.model; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class AccountId implements Serializable { + + private static final long serialVersionUID = 1L; + + private String accountNumber; + private String accountType; + +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/AnnotationDescriptorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/AnnotationDescriptorTest.java new file mode 100644 index 000000000..182707b07 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/AnnotationDescriptorTest.java @@ -0,0 +1,39 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import javax.persistence.Entity; + +import org.junit.Test; + + +public class AnnotationDescriptorTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(AnnotationSampeBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + AnnotationDescriptor subject = classDescriptor.getAnnotationDescriptor(Entity.class); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + + } + + @Entity + static class AnnotationSampeBean { + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/AnnotationsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/AnnotationsTest.java new file mode 100644 index 000000000..7a3c75086 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/AnnotationsTest.java @@ -0,0 +1,45 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +public class AnnotationsTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(AnnotationsSampeBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + Annotations subject = classDescriptor.getAnnotations(); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class AnnotationsSampeBean { + + private String foo; + private String bar; + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ArrayUtilTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ArrayUtilTest.java new file mode 100644 index 000000000..1ccf91f88 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ArrayUtilTest.java @@ -0,0 +1,66 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + + +public class ArrayUtilTest { + + @Test + public void testIsEmpty() { + assertThat(ArrayUtil.isEmpty(null)).isTrue(); + + assertThat(ArrayUtil.isEmpty(new String[0])).isTrue();; + assertThat(ArrayUtil.isEmpty(new String[10])).isFalse(); + + assertThat(ArrayUtil.isEmpty(new int[0])).isTrue();; + assertThat(ArrayUtil.isEmpty(new int[10])).isFalse(); + + assertThat(ArrayUtil.isEmpty(new Object())).isFalse(); // not an array + } + + @Test + public void testIsNotEmpty() { + assertThat(ArrayUtil.isNotEmpty(null)).isFalse(); + + assertThat(ArrayUtil.isNotEmpty(new String[0])).isFalse();; + assertThat(ArrayUtil.isNotEmpty(new String[10])).isTrue(); + + assertThat(ArrayUtil.isNotEmpty(new int[0])).isFalse();; + assertThat(ArrayUtil.isNotEmpty(new int[10])).isTrue(); + + assertThat(ArrayUtil.isNotEmpty(new Object())).isTrue(); // not an array + } + + @Test + public void testIndexOfObject() { + assertThat(ArrayUtil.indexOf(null, "a")).isEqualTo(-1); + assertThat(ArrayUtil.indexOf(new String[] { "a", null, "c" }, (String) null)).isEqualTo(1); + assertThat(ArrayUtil.indexOf(new String[] { "a", "b", "c" }, (String) null)).isEqualTo(-1); + assertThat(ArrayUtil.indexOf(new String[0], "a")).isEqualTo(-1); + assertThat(ArrayUtil.indexOf(new String[] { "a", "a", "b", "a", "a", "b", "a", "a" }, "a")).isEqualTo(0); + assertThat(ArrayUtil.indexOf(new String[] { "a", "a", "b", "a", "a", "b", "a", "a" }, "b")).isEqualTo(2); + + assertThat(ArrayUtil.indexOf(null, "a", 0)).isEqualTo(-1); + assertThat(ArrayUtil.indexOf(new String[] { "a", null, "c" }, (String) null, 0)).isEqualTo(1); + assertThat(ArrayUtil.indexOf(new String[0], "a", 0)).isEqualTo(-1); + assertThat(ArrayUtil.indexOf(new String[] { "a", "a", "b", "a", "a", "b", "a", "a" }, "b", 0)).isEqualTo(2); + assertThat(ArrayUtil.indexOf(new String[] { "a", "a", "b", "a", "a", "b", "a", "a" }, "b", 3)).isEqualTo(5); + assertThat(ArrayUtil.indexOf(new String[] { "a", "a", "b", "a", "a", "b", "a", "a" }, "b", 9)).isEqualTo(-1); + assertThat(ArrayUtil.indexOf(new String[] { "a", "a", "b", "a", "a", "b", "a", "a" }, "b", -1)).isEqualTo(2); + } + + @Test + public void testAddAll() { + assertThat(ArrayUtil.addAll(null, null)).isNull(); + assertThat(ArrayUtil.addAll(null, new String[] {})).isEqualTo(new String[] {}); + assertThat(ArrayUtil.addAll(new String[] {}, null)).isEqualTo(new String[] {}); + assertThat(ArrayUtil.addAll(new String[] {}, new String[] {})).isEqualTo(new String[] {}); + assertThat(ArrayUtil.addAll(null, new String[] {"a"})).isEqualTo(new String[] {"a"}); + assertThat(ArrayUtil.addAll(new String[] {"a"}, null)).isEqualTo(new String[] {"a"}); + assertThat(ArrayUtil.addAll(new String[] {"a"}, new String[] {"b"})).isEqualTo(new String[] {"a", "b"}); + } + + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/BeanUtilTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/BeanUtilTest.java new file mode 100644 index 000000000..9ccc3d5e5 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/BeanUtilTest.java @@ -0,0 +1,128 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; + +import org.junit.Test; + + +public class BeanUtilTest { + + @Test + public void testGetBeanGetterNameNull() { + assertThat(BeanUtil.getBeanGetterName(null)).isNull(); + } + + @Test + public void testGetBeanSetterNameNull() { + assertThat(BeanUtil.getBeanSetterName(null)).isNull(); + } + + @Test + public void testIsBeanSetterNameNull() { + assertThat(BeanUtil.isBeanGetter(null)).isFalse(); + } + + @Test + public void testIsBeanGetterGetMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = TestBean.class.getDeclaredMethod("getFoo", new Class[] {}); + + // then + assertThat(BeanUtil.isBeanGetter(subject)).isTrue(); + } + + @Test + public void testIsBeanGetterIsMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = TestBean.class.getDeclaredMethod("isBar", new Class[] {}); + + // then + assertThat(BeanUtil.isBeanGetter(subject)).isTrue(); + } + + @Test + public void testIsBeanGetterToString() throws NoSuchMethodException, SecurityException { + // given + Method subject = TestBean.class.getDeclaredMethod("toString", new Class[] {}); + + // then + assertThat(BeanUtil.isBeanGetter(subject)).isFalse(); + } + + @Test + public void testGetBeanGetterNameObjectMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = Object.class.getDeclaredMethod("hashCode", new Class[] {}); + + // then + assertThat(BeanUtil.getBeanGetterName(subject)).isNull(); + } + + @Test + public void testIsBeanPropertyObjectMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = Object.class.getDeclaredMethod("hashCode", new Class[] {}); + + // then + assertThat(BeanUtil.isBeanProperty(subject)).isFalse(); + } + + @Test + public void testIsBeanPropertyGetMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = TestBean.class.getDeclaredMethod("getFoo", new Class[] {}); + + // then + assertThat(BeanUtil.isBeanProperty(subject)).isTrue(); + } + + @Test + public void testIsBeanPropertyIsMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = TestBean.class.getDeclaredMethod("isBar", new Class[] {}); + + // then + assertThat(BeanUtil.isBeanProperty(subject)).isTrue(); + } + + @Test + public void testIsBeanPropertySetMethod() throws NoSuchMethodException, SecurityException { + // given + Method subject = TestBean.class.getDeclaredMethod("setBar", new Class[] { boolean.class }); + + // then + assertThat(BeanUtil.isBeanProperty(subject)).isTrue(); + } + + + static class TestBean { + private String foo; + private boolean bar; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public boolean isBar() { + return bar; + } + + public void setBar(boolean bar) { + this.bar = bar; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("TestBean [foo=").append(foo).append(", bar=").append(bar).append("]"); + return builder.toString(); + } + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ClassIntrospectorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ClassIntrospectorTest.java new file mode 100644 index 000000000..1b6aebc34 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ClassIntrospectorTest.java @@ -0,0 +1,725 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import javax.persistence.Entity; + +import org.junit.Test; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; + +import lombok.Data; + + +public class ClassIntrospectorTest { + + private static ClassIntrospector introspector = ClassIntrospector.builder() + .withEnhancedProperties(true) + .withScanAccessible(true) + .withIncludeFieldsAsProperties(true) + .withScanStatics(false) + .build(); + + @Test + public void testBasic() { + ClassDescriptor cd = introspector.introspect(BeanSampleA.class); + assertNotNull(cd); + PropertyDescriptor[] properties = cd.getAllPropertyDescriptors(); + int c = 0; + for (PropertyDescriptor property : properties) { + if (property.isFieldOnlyDescriptor()) + continue; + if (property.isPublic()) + c++; + } + assertEquals(2, c); + + Arrays.sort(properties, new Comparator() { + @Override + public int compare(PropertyDescriptor o1, PropertyDescriptor o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + PropertyDescriptor pd = properties[0]; + assertEquals("fooProp", pd.getName()); + assertNotNull(pd.getReadMethodDescriptor()); + assertNotNull(pd.getWriteMethodDescriptor()); + assertNotNull(pd.getFieldDescriptor()); + + pd = properties[1]; + assertEquals("shared", pd.getName()); + assertNull(pd.getReadMethodDescriptor()); + assertNull(pd.getWriteMethodDescriptor()); + assertNotNull(pd.getFieldDescriptor()); + + pd = properties[2]; + assertEquals("something", pd.getName()); + assertNotNull(pd.getReadMethodDescriptor()); + assertNull(pd.getWriteMethodDescriptor()); + assertNull(pd.getFieldDescriptor()); + + assertNotNull(cd.getPropertyDescriptor("fooProp", false)); + assertNotNull(cd.getPropertyDescriptor("something", false)); + assertNull(cd.getPropertyDescriptor("FooProp", false)); + assertNull(cd.getPropertyDescriptor("Something", false)); + assertNull(cd.getPropertyDescriptor("notExisting", false)); + } + + @Test + public void testExtends() { + ClassDescriptor cd = introspector.introspect(BeanSampleB.class); + assertNotNull(cd); + + PropertyDescriptor[] properties = cd.getAllPropertyDescriptors(); + int c = 0; + for (PropertyDescriptor property : properties) { + if (property.isFieldOnlyDescriptor()) + continue; + if (property.isPublic()) + c++; + } + assertEquals(2, c); + + c = 0; + for (PropertyDescriptor property : properties) { + if (property.isFieldOnlyDescriptor()) + continue; + c++; + } + assertEquals(3, c); + assertEquals(4, properties.length); + + Arrays.sort(properties, new Comparator() { + @Override + public int compare(PropertyDescriptor o1, PropertyDescriptor o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + PropertyDescriptor pd = properties[0]; + assertEquals("boo", pd.getName()); + assertNotNull(pd.getReadMethodDescriptor()); + assertNotNull(pd.getWriteMethodDescriptor()); + assertNotNull(pd.getFieldDescriptor()); + assertFalse(pd.isFieldOnlyDescriptor()); + + pd = properties[1]; + assertEquals("fooProp", pd.getName()); + assertNotNull(pd.getReadMethodDescriptor()); + assertNotNull(pd.getWriteMethodDescriptor()); + assertNull(pd.getFieldDescriptor()); // null since field is not visible + assertFalse(pd.isFieldOnlyDescriptor()); + + pd = properties[2]; + assertEquals("shared", pd.getName()); + assertNull(pd.getReadMethodDescriptor()); + assertNull(pd.getWriteMethodDescriptor()); + assertNotNull(pd.getFieldDescriptor()); + assertTrue(pd.isFieldOnlyDescriptor()); + + pd = properties[3]; + assertEquals("something", pd.getName()); + assertNotNull(pd.getReadMethodDescriptor()); + assertNull(pd.getWriteMethodDescriptor()); + assertNull(pd.getFieldDescriptor()); + assertFalse(pd.isFieldOnlyDescriptor()); + + assertNotNull(cd.getPropertyDescriptor("fooProp", false)); + assertNotNull(cd.getPropertyDescriptor("something", false)); + assertNull(cd.getPropertyDescriptor("FooProp", false)); + assertNull(cd.getPropertyDescriptor("Something", false)); + assertNull(cd.getPropertyDescriptor("notExisting", false)); + + assertNotNull(cd.getPropertyDescriptor("boo", true)); + assertNull(cd.getPropertyDescriptor("boo", false)); + } + + @Test + public void testCtors() { + ClassDescriptor cd = introspector.introspect(Parent.class); + ConstructorDescriptor[] ctors = cd.getAllConstructorDescriptors(); + int c = 0; + for (ConstructorDescriptor ctor : ctors) { + if (ctor.isPublic()) + c++; + } + assertEquals(1, c); + ctors = cd.getAllConstructorDescriptors(); + assertEquals(2, ctors.length); + assertNotNull(cd.getDefaultCtorDescriptor(true)); + assertNull(cd.getDefaultCtorDescriptor(false)); + + Constructor ctor = cd.getConstructorDescriptor(new Class[] { Integer.class }, true).getConstructor(); + assertNotNull(ctor); + + cd = introspector.introspect(Child.class); + ctors = cd.getAllConstructorDescriptors(); + c = 0; + for (ConstructorDescriptor ccc : ctors) { + if (ccc.isPublic()) + c++; + } + assertEquals(1, c); + + ctors = cd.getAllConstructorDescriptors(); + assertEquals(1, ctors.length); + assertNull(cd.getDefaultCtorDescriptor(false)); + assertNull(cd.getDefaultCtorDescriptor(true)); + + ConstructorDescriptor ctorDescriptor = cd.getConstructorDescriptor(new Class[] { Integer.class }, true); + assertNull(ctorDescriptor); + ctor = cd.getConstructorDescriptor(new Class[] { String.class }, true).getConstructor(); + assertNotNull(ctor); + } + + @Test + public void testSameFieldDifferentClass() { + ClassDescriptor cd = introspector.introspect(BeanSampleA.class); + + FieldDescriptor fd = cd.getFieldDescriptor("shared", false); + assertNull(fd); + + fd = cd.getFieldDescriptor("shared", true); + assertNotNull(fd); + + ClassDescriptor cd2 = introspector.introspect(BeanSampleB.class); + FieldDescriptor fd2 = cd2.getFieldDescriptor("shared", true); + + assertNotEquals(fd, fd2); + assertEquals(fd.getField(), fd2.getField()); + } + + @Test + public void testPropertyMatches() { + ClassDescriptor cd = introspector.introspect(BeanSampleC.class); + + PropertyDescriptor pd; + + pd = cd.getPropertyDescriptor("s1", false); + assertNull(pd); + + pd = cd.getPropertyDescriptor("s1", true); + assertFalse(pd.isPublic()); + assertTrue(pd.getReadMethodDescriptor().isPublic()); + assertFalse(pd.getWriteMethodDescriptor().isPublic()); + + assertNotNull(getPropertyGetterDescriptor(cd, "s1", false)); + assertNull(getPropertySetterDescriptor(cd, "s1", false)); + + pd = cd.getPropertyDescriptor("s2", false); + assertNull(pd); + + pd = cd.getPropertyDescriptor("s2", true); + assertFalse(pd.isPublic()); + assertFalse(pd.getReadMethodDescriptor().isPublic()); + assertTrue(pd.getWriteMethodDescriptor().isPublic()); + + assertNull(getPropertyGetterDescriptor(cd, "s2", false)); + assertNotNull(getPropertySetterDescriptor(cd, "s2", false)); + + pd = cd.getPropertyDescriptor("s3", false); + assertNotNull(pd); + + pd = cd.getPropertyDescriptor("s3", true); + assertTrue(pd.isPublic()); + assertTrue(pd.getReadMethodDescriptor().isPublic()); + assertTrue(pd.getWriteMethodDescriptor().isPublic()); + + assertNotNull(getPropertyGetterDescriptor(cd, "s3", false)); + assertNotNull(getPropertySetterDescriptor(cd, "s3", false)); + } + + @Test + public void testOverload() { + ClassDescriptor cd = introspector.introspect(Overload.class); + + PropertyDescriptor[] pds = cd.getAllPropertyDescriptors(); + + assertEquals(1, pds.length); + + PropertyDescriptor pd = pds[0]; + + assertNotNull(pd.getFieldDescriptor()); + assertNotNull(pd.getReadMethodDescriptor()); + assertNull(pd.getWriteMethodDescriptor()); + } + + @Test + public void testSerialUid() { + ClassDescriptor cd = introspector.introspect(BeanSampleB.class); + + assertNull(cd.getFieldDescriptor("serialVersionUID", true)); + } + + @Test + public void testStaticField() { + ClassDescriptor cd = introspector.introspect(BeanSampleA.class); + + assertNull(cd.getFieldDescriptor("staticField", true)); + } + + @Test + public void testStaticMethod() { + ClassDescriptor cd = introspector.introspect(BeanSampleB.class); + + assertNull(cd.getMethodDescriptor("staticMethod", true)); + } + + @Test + public void testFields() throws NoSuchFieldException { + ClassDescriptor cd = introspector.introspect(MethodParameterType.class); + + assertEquals(MethodParameterType.class, cd.getType()); + assertEquals(4, cd.getAllFieldDescriptors().length); + + FieldDescriptor[] fs = cd.getAllFieldDescriptors(); + int p = 0; + for (FieldDescriptor f : fs) { + if (f.isPublic()) { + p++; + } + } + assertEquals(0, p); + + FieldDescriptor fd = cd.getFieldDescriptor("f", true); + FieldDescriptor fd2 = cd.getFieldDescriptor("f2", true); + FieldDescriptor fd3 = cd.getFieldDescriptor("f3", true); + FieldDescriptor fd4 = cd.getFieldDescriptor("f4", true); + + assertEquals(List.class, fd.getRawType()); + assertEquals(Object.class, fd.getRawComponentType()); + + assertEquals(List.class, fd2.getRawType()); + assertEquals(Object.class, fd2.getRawComponentType()); + + assertEquals(Map.class, fd3.getRawType()); + assertEquals(Object.class, fd3.getRawComponentType()); + + assertEquals(List.class, fd4.getRawType()); + assertEquals(Long.class, fd4.getRawComponentType()); + + // impl + cd = introspector.introspect(Foo.class); + + fd = cd.getFieldDescriptor("f", true); + fd2 = cd.getFieldDescriptor("f2", true); + fd3 = cd.getFieldDescriptor("f3", true); + + assertEquals(List.class, fd.getRawType()); + assertEquals(Integer.class, fd.getRawComponentType()); + + assertEquals(List.class, fd2.getRawType()); + assertEquals(Object.class, fd2.getRawComponentType()); + + assertEquals(Map.class, fd3.getRawType()); + assertEquals(Integer.class, fd3.getRawComponentType()); + assertEquals(String.class, ReflectionUtil.getComponentTypes(fd3.getField().getGenericType(), cd.getType())[0]); + } + + @Test + public void testMethods() throws NoSuchMethodException { + ClassDescriptor cd = introspector.introspect(MethodParameterType.class); + + assertEquals(MethodParameterType.class, cd.getType()); + assertEquals(5, cd.getAllMethodDescriptors().length); + + MethodDescriptor[] mds = cd.getAllMethodDescriptors(); + int mc = 0; + for (MethodDescriptor md : mds) { + if (md.isPublic()) + mc++; + } + assertEquals(0, mc); + + Class[] params = new Class[] { Object.class, String.class, List.class, List.class, List.class }; + + Method m = MethodParameterType.class.getDeclaredMethod("m", params); + assertNotNull(m); + + Method m2 = cd.getMethodDescriptor("m", params, true).getMethod(); + assertNotNull(m2); + assertEquals(m, m2); + + MethodDescriptor md1 = cd.getMethodDescriptor("m", params, true); + assertNotNull(md1); + assertEquals(m, md1.getMethod()); + assertArrayEquals(params, md1.getRawParameterTypes()); + assertEquals(void.class, md1.getRawReturnType()); + assertNull(md1.getRawReturnComponentType()); + + MethodDescriptor md2 = cd.getMethodDescriptor("m2", params, true); + assertNotNull(md2); + assertArrayEquals(params, md2.getRawParameterTypes()); + assertEquals(List.class, md2.getRawReturnType()); + assertEquals(List.class, md2.getRawReturnComponentType()); + + MethodDescriptor md3 = cd.getMethodDescriptor("m3", params, true); + assertNotNull(md3); + assertArrayEquals(params, md3.getRawParameterTypes()); + assertEquals(List.class, md3.getRawReturnType()); + assertEquals(Object.class, md3.getRawReturnComponentType()); + + MethodDescriptor md4 = cd.getMethodDescriptor("m4", new Class[] { List.class }, true); + assertNotNull(md4); + assertArrayEquals(new Class[] { List.class }, md4.getRawParameterTypes()); + assertEquals(List.class, md4.getRawReturnType()); + assertEquals(Byte.class, md4.getRawReturnComponentType()); + assertEquals(List.class, md4.getSetterRawType()); + assertEquals(Long.class, md4.getSetterRawComponentType()); + + MethodDescriptor md5 = cd.getMethodDescriptor("m5", new Class[] { List.class }, true); + assertNotNull(md5); + assertArrayEquals(new Class[] { List.class }, md5.getRawParameterTypes()); + assertEquals(List.class, md5.getRawReturnType()); + assertEquals(Object.class, md5.getRawReturnComponentType()); + assertEquals(List.class, md5.getSetterRawType()); + assertEquals(Object.class, md5.getSetterRawComponentType()); + + Class[] params2 = new Class[] { Integer.class, String.class, List.class, List.class, List.class }; + + ClassDescriptor cd1 = introspector.introspect(Foo.class); + + MethodDescriptor[] allm = cd1.getAllMethodDescriptors(); + + assertEquals(5, allm.length); + + md3 = cd1.getMethodDescriptor("m", params, true); + assertNotNull(md3); + + assertArrayEquals(params2, md3.getRawParameterTypes()); + + md3 = cd1.getMethodDescriptor("m3", params, true); + assertNotNull(md3); + assertArrayEquals(params2, md3.getRawParameterTypes()); + assertEquals(List.class, md3.getRawReturnType()); + assertEquals(Integer.class, md3.getRawReturnComponentType()); + + md5 = cd1.getMethodDescriptor("m5", new Class[] { List.class }, true); + assertNotNull(md5); + assertArrayEquals(new Class[] { List.class }, md5.getRawParameterTypes()); + assertEquals(List.class, md5.getRawReturnType()); + assertEquals(Integer.class, md5.getRawReturnComponentType()); + assertEquals(List.class, md5.getSetterRawType()); + assertEquals(Integer.class, md5.getSetterRawComponentType()); + } + + @Test + public void testClassAnnotations() throws NoSuchFieldException, SecurityException { + // given + Annotation classAnnotation = BeanSampleD.class.getAnnotation(Entity.class); + AnnotationDescriptor expected = new AnnotationDescriptor(classAnnotation); + + // when + ClassDescriptor classDescriptor = introspector.introspect(BeanSampleD.class); + + // then + assertThat(classDescriptor.getAllAnnotationDescriptors()) + .containsOnly(expected) + .extracting(AnnotationDescriptor::getAnnotation, + AnnotationDescriptor::getAnnotationType, + AnnotationDescriptor::getElementTypes, + AnnotationDescriptor::getPolicy, + AnnotationDescriptor::isDocumented, + AnnotationDescriptor::isInherited) + .contains(tuple(classAnnotation, + Entity.class, + new ElementType[] {ElementType.TYPE}, + expected.getPolicy(), + true, + false)); + } + + @Test + public void testFieldAnnotations() throws NoSuchFieldException, SecurityException { + // given + Annotation fieldAnnotation = BeanSampleD.class.getDeclaredField("foo") + .getAnnotation(GraphQLIgnore.class); + AnnotationDescriptor expected = new AnnotationDescriptor(fieldAnnotation); + + // when + FieldDescriptor subject = introspector.introspect(BeanSampleD.class) + .getFieldDescriptor("foo", true); + // then + assertThat(subject.getAllAnnotationDescriptors()) + .containsOnly(expected) + .extracting(AnnotationDescriptor::getAnnotation, + AnnotationDescriptor::getAnnotationType, + AnnotationDescriptor::getElementTypes, + AnnotationDescriptor::getPolicy, + AnnotationDescriptor::isDocumented, + AnnotationDescriptor::isInherited) + .contains(tuple(fieldAnnotation, + GraphQLIgnore.class, + new ElementType[] {ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}, + expected.getPolicy(), + false, + false)); + + } + + @Test + public void testMethodAnnotations() throws NoSuchFieldException, SecurityException, NoSuchMethodException { + // given + Annotation fieldAnnotation = BeanSampleD.class.getDeclaredMethod("getBar") + .getAnnotation(GraphQLDescription.class); + AnnotationDescriptor expected = new AnnotationDescriptor(fieldAnnotation); + + // when + MethodDescriptor subject = introspector.introspect(BeanSampleD.class) + .getMethodDescriptor("getBar", new Class[] {}, true); + // then + assertThat(subject.getAllAnnotationDescriptors()) + .containsOnly(expected) + .extracting(AnnotationDescriptor::getAnnotation, + AnnotationDescriptor::getAnnotationType, + AnnotationDescriptor::getElementTypes, + AnnotationDescriptor::getPolicy, + AnnotationDescriptor::isDocumented, + AnnotationDescriptor::isInherited) + .contains(tuple(fieldAnnotation, + GraphQLDescription.class, + new ElementType[] {ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}, + expected.getPolicy(), + false, + false)); + } + + @Test + public void testConstructorDescriptors() throws NoSuchFieldException, SecurityException, NoSuchMethodException { + // given + ClassDescriptor classDescriptor = introspector.introspect(BeanSampleD.class); + Constructor constructor = BeanSampleD.class.getConstructor(new Class[] {}); + + // when + ConstructorDescriptor subject = introspector.introspect(BeanSampleD.class) + .getConstructorDescriptor(new Class[] {}, true); + // then + assertThat(classDescriptor.getAllConstructorDescriptors()) + .containsOnly(subject) + .extracting(ConstructorDescriptor::getConstructor, + ConstructorDescriptor::getClassDescriptor, + ConstructorDescriptor::getName, + ConstructorDescriptor::getParameters, + ConstructorDescriptor::isDefault, + ConstructorDescriptor::isPublic) + .contains(tuple(constructor, + classDescriptor, + "com.introproventures.graphql.jpa.query.introspection.ClassIntrospectorTest$BeanSampleD", + new Class[] {}, + true, + true)); + } + + + @Test + public void testPropertyDescriptors() throws NoSuchFieldException, SecurityException, NoSuchMethodException { + // given + ClassDescriptor classDescriptor = introspector.introspect(BeanSampleD.class); + + // when + PropertyDescriptor[] subject = introspector.introspect(BeanSampleD.class) + .getAllPropertyDescriptors(); + // then + assertThat(subject).hasSize(2) + .extracting(PropertyDescriptor::getName, + PropertyDescriptor::getType, + PropertyDescriptor::getClassDescriptor, + PropertyDescriptor::isFieldOnlyDescriptor, + PropertyDescriptor::isPublic) + .contains(tuple("bar", String.class, classDescriptor, false, false), + tuple("foo", Integer.class, classDescriptor, false, true)); + } + + + + @Data + @Entity + static class BeanSampleD { + + @GraphQLIgnore + protected Integer foo; + + private String bar; + + @GraphQLDescription("getBar") + protected String getBar() { + return bar; + } + } + + static class BeanSampleA { + + protected static String staticField; + + protected Integer shared; + + private String fooProp = "abean_value"; + + public void setFooProp(String v) { + fooProp = v; + } + + public String getFooProp() { + return fooProp; + } + + public boolean isSomething() { + return true; + } + } + + static class BeanSampleB extends BeanSampleA { + + public static final long serialVersionUID = 42L; + + public static void staticMethod() { }; + + private Long boo; + + Long getBoo() { + return boo; + } + + void setBoo(Long boo) { + this.boo = boo; + } + } + + public class BeanSampleC { + + private String s1; + private String s2; + private String s3; + + public String getS1() { + return s1; + } + + protected void setS1(String s1) { + this.s1 = s1; + } + + protected String getS2() { + return s2; + } + + public void setS2(String s2) { + this.s2 = s2; + } + + public String getS3() { + return s3; + } + + public void setS3(String s3) { + this.s3 = s3; + } + } + + static class Parent { + + protected Parent() { + + } + + public Parent(Integer i) { + + } + + } + + static class Child extends Parent { + + public Child(String a) { + super(); + } + } + + static class Overload { + + String company; + + // not a property setter + public void setCompany(StringBuilder sb) { + this.company = sb.toString(); + } + + public String getCompany() { + return company; + } + } + + static class MethodParameterType { + List f; + List f2; + Map f3; + List f4; + + > void m(A a, String p1, T p2, List p3, List p4) { + } + + > List m2(A a, String p1, T p2, List p3, List p4) { + return null; + } + + > List m3(A a, String p1, T p2, List p3, List p4) { + return null; + } + + List m4(List list) { + return null; + } + + List m5(List list) { + return null; + } + } + + static class Foo extends MethodParameterType { + } + + MethodDescriptor getPropertySetterDescriptor(ClassDescriptor cd, String name, boolean declared) { + PropertyDescriptor propertyDescriptor = cd.getPropertyDescriptor(name, true); + + if (propertyDescriptor != null) { + MethodDescriptor setter = propertyDescriptor.getWriteMethodDescriptor(); + + if ((setter != null) && setter.matchDeclared(declared)) { + return setter; + } + } + return null; + } + + MethodDescriptor getPropertyGetterDescriptor(ClassDescriptor cd, String name, boolean declared) { + PropertyDescriptor propertyDescriptor = cd.getPropertyDescriptor(name, true); + + if (propertyDescriptor != null) { + MethodDescriptor getter = propertyDescriptor.getReadMethodDescriptor(); + + if ((getter != null) && getter.matchDeclared(declared)) { + return getter; + } + } + return null; + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ClassUtilTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ClassUtilTest.java new file mode 100644 index 000000000..c9798bf8b --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ClassUtilTest.java @@ -0,0 +1,69 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; +import java.util.RandomAccess; + +import org.junit.Test; + + +public class ClassUtilTest { + + @Test + public void getAllInterfaces() { + assertEquals(null, ClassUtil.getAllInterfaces(null)); + assertTrue(ClassUtil.getAllInterfaces(Object.class).isEmpty()); + + assertFalse(ClassUtil.getAllInterfaces(String.class).isEmpty()); + assertFalse(ClassUtil.getAllInterfaces(Class.class).isEmpty()); + + List> supers = ClassUtil.getAllInterfaces(ArrayList.class); + assertFalse(supers.contains(AbstractList.class)); + assertFalse(supers.contains(AbstractCollection.class)); + + assertTrue(supers.contains(List.class)); + assertTrue(supers.contains(RandomAccess.class)); + assertTrue(supers.contains(Iterable.class)); + assertFalse(supers.contains(Object.class)); + + assertTrue(ClassUtil.getAllInterfaces(int.class).isEmpty()); + // assertTrue(ClassUtil.getAllInterfaces(int[].class).isEmpty()); + assertFalse(ClassUtil.getAllInterfaces(Integer.class).contains(Number.class)); + assertTrue(ClassUtil.getAllInterfaces(Integer.class).contains(Comparable.class)); + + List> list = ClassUtil.getAllInterfaces(Integer[].class); + assertTrue(list.contains(Cloneable.class)); + assertTrue(list.contains(Serializable.class)); + + } + + @Test + public void getAllSuperclasses() { + assertEquals(null, ClassUtil.getAllSuperclasses(null)); + assertTrue(ClassUtil.getAllSuperclasses(Object.class).isEmpty()); + + assertTrue(ClassUtil.getAllSuperclasses(String.class).isEmpty()); + + List> supers = ClassUtil.getAllSuperclasses(ArrayList.class); + assertTrue(supers.contains(AbstractList.class)); + assertTrue(supers.contains(AbstractCollection.class)); + + assertFalse(supers.contains(List.class)); + assertFalse(supers.contains(Object.class)); + + assertTrue(ClassUtil.getAllSuperclasses(int.class).isEmpty()); + assertTrue(ClassUtil.getAllSuperclasses(int[].class).isEmpty()); + assertTrue(ClassUtil.getAllSuperclasses(Integer.class).contains(Number.class)); + + assertTrue(ClassUtil.getAllSuperclasses(Integer[].class).isEmpty()); + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ConstructorDescriptorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ConstructorDescriptorTest.java new file mode 100644 index 000000000..cc1bc064e --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ConstructorDescriptorTest.java @@ -0,0 +1,61 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +public class ConstructorDescriptorTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(SampleBean.class); + + @Test + public void testToStringEqualsHashCode() { + ConstructorDescriptor subject = classDescriptor.getConstructorDescriptor(new Class[] {}, true); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Test + public void testGetDeclaringClass() { + ConstructorDescriptor subject = classDescriptor.getConstructorDescriptor(new Class[] {}, true); + + // then + assertThat(subject.getDeclaringClass()).isEqualTo(SampleBean.class); + } + + @Test + public void testParameters() { + ConstructorDescriptor subject = classDescriptor.getConstructorDescriptor(new Class[] {}, true); + + // then + assertThat(subject.getParameters()).isEqualTo(new Class[] {}); + } + + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class SampleBean { + private String foo; + private String bar; + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ConstructorsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ConstructorsTest.java new file mode 100644 index 000000000..ad6e300db --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ConstructorsTest.java @@ -0,0 +1,44 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +public class ConstructorsTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(ConstructorsSampeBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + Constructors subject = classDescriptor.getConstructors(); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class ConstructorsSampeBean { + private String foo; + private String bar; + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/FieldDescriptorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/FieldDescriptorTest.java new file mode 100644 index 000000000..41d0874c5 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/FieldDescriptorTest.java @@ -0,0 +1,99 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.lang.reflect.InvocationTargetException; +import java.util.Optional; + +import org.junit.Test; + +import lombok.Data; + + +public class FieldDescriptorTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(FieldsSampleBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + FieldDescriptor subject = classDescriptor.getFieldDescriptor("id", true); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + + } + + @Test + public void testFieldDescriptor() { + FieldDescriptor subject = classDescriptor.getFieldDescriptor("nickName", true); + + // then + assertThat(subject).isNotNull() + .extracting(FieldDescriptor::getName, + FieldDescriptor::getDeclaringClass, + FieldDescriptor::getRawType, + FieldDescriptor::getRawComponentType, + FieldDescriptor::getRawKeyComponentType, + FieldDescriptor::getGetterRawComponentType, + FieldDescriptor::getGetterRawKeyComponentType, + FieldDescriptor::getSetterRawType, + FieldDescriptor::getSetterRawComponentType) + .contains("nickName", + FieldsSampleBean.class, + Optional.class, + String.class, + String.class, + String.class, + String.class, + String.class, + String.class); + } + + + @Test + public void testInvokeGetter() throws InvocationTargetException, IllegalAccessException { + FieldDescriptor subject = classDescriptor.getFieldDescriptor("id", true); + FieldsSampleBean target = new FieldsSampleBean("id"); + + + // when + Object result = subject.invokeGetter(target); + + // then + assertThat(result).isEqualTo("id"); + } + + @Test + public void testInvokeSetter() throws InvocationTargetException, IllegalAccessException { + FieldDescriptor subject = classDescriptor.getFieldDescriptor("name", true); + FieldsSampleBean target = new FieldsSampleBean("id"); + + // when + subject.invokeSetter(target, "name"); + + // then + assertThat(target.getName()).isEqualTo("name"); + } + + + @Data + static class FieldsSampleBean { + private final String id; + private String name; + private Optional nickName = Optional.empty(); + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/FieldsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/FieldsTest.java new file mode 100644 index 000000000..28afce115 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/FieldsTest.java @@ -0,0 +1,44 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +public class FieldsTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(FieldsSampeBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + Fields subject = classDescriptor.getFields(); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class FieldsSampeBean { + + private String foo; + private String bar; + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/MethodDescriptorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/MethodDescriptorTest.java new file mode 100644 index 000000000..e71a5c6ee --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/MethodDescriptorTest.java @@ -0,0 +1,122 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Optional; + +import org.junit.Test; + +import lombok.Data; + +public class MethodDescriptorTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(MethodsSampleBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + MethodDescriptor subject = classDescriptor.getMethodDescriptor("getId", true); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + + } + + @Test + public void testGetMethodDescriptor() { + MethodDescriptor subject = classDescriptor.getMethodDescriptor("getNickName", true); + + // then + assertThat(subject).isNotNull() + .extracting(MethodDescriptor::getName, + MethodDescriptor::getDeclaringClass, + MethodDescriptor::getGetterRawType, + MethodDescriptor::getGetterRawComponentType, + MethodDescriptor::getGetterRawKeyComponentType, + MethodDescriptor::getRawParameterTypes, + MethodDescriptor::getRawReturnType) + .contains("getNickName", + MethodsSampleBean.class, + Optional.class, + String.class, + String.class, + Optional.class, + String.class, + String.class); + } + + @Test + public void testSetMethodDescriptor() throws NoSuchMethodException, SecurityException { + MethodDescriptor subject = classDescriptor.getMethodDescriptor("setNickName", true); + Method method = MethodsSampleBean.class.getDeclaredMethod("setNickName", new Class[] {Optional.class}); + + // then + assertThat(subject).isNotNull() + .extracting(MethodDescriptor::getName, + MethodDescriptor::getMethod, + MethodDescriptor::isPublic, + MethodDescriptor::getDeclaringClass, + MethodDescriptor::getGetterRawType, + MethodDescriptor::getGetterRawComponentType, + MethodDescriptor::getGetterRawKeyComponentType, + MethodDescriptor::getRawParameterTypes, + MethodDescriptor::getRawReturnType) + .contains("setNickName", + method, + true, + MethodsSampleBean.class, + void.class, + null, + null, + new Class[] {Optional.class}, + void.class); + } + + + @Test + public void testInvokeGetter() throws InvocationTargetException, IllegalAccessException { + MethodDescriptor subject = classDescriptor.getMethodDescriptor("getId", true); + MethodsSampleBean target = new MethodsSampleBean("id"); + + // when + Object result = subject.invokeGetter(target); + + // then + assertThat(result).isEqualTo("id"); + } + + @Test + public void testInvokeSetter() throws InvocationTargetException, IllegalAccessException { + MethodDescriptor subject = classDescriptor.getMethodDescriptor("setName", true); + MethodsSampleBean target = new MethodsSampleBean("id"); + + // when + subject.invokeSetter(target, "name"); + + // then + assertThat(target.getName()).isEqualTo("name"); + } + + @Data + static class MethodsSampleBean { + + private final String id; + private String name; + private Optional nickName = Optional.empty(); + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/MethodsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/MethodsTest.java new file mode 100644 index 000000000..7e521a534 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/MethodsTest.java @@ -0,0 +1,45 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +public class MethodsTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(MethodsSampeBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + Methods subject = classDescriptor.getMethods(); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class MethodsSampeBean { + + private String foo; + private String bar; + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ObjectUtilTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ObjectUtilTest.java new file mode 100644 index 000000000..ff7efe1b3 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ObjectUtilTest.java @@ -0,0 +1,82 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import org.junit.Test; + + +public class ObjectUtilTest { + + @Test + public void testNull() { + // all null + Object[] NULL = null; + + // any null + assertTrue(ObjectUtil.isAnyNull(NULL)); + assertTrue(ObjectUtil.isAnyNull(new Object[] { null, null, null, null, null })); + assertTrue(ObjectUtil.isAnyNull(new Object[] { null, null, 0, null, null })); + assertTrue(ObjectUtil.isAnyNull(new Object[] { null, "null", 0, null, null })); + assertFalse(ObjectUtil + .isAnyNull(new Object[] { "", "null", 0, new int[] {}, new ArrayList<>() })); + } + + @Test + public void isEquals() throws Exception { + assertTrue(ObjectUtil.isEquals(null, null)); + assertFalse(ObjectUtil.isEquals(null, "")); + assertFalse(ObjectUtil.isEquals("", null)); + assertTrue(ObjectUtil.isEquals("", "")); + assertFalse(ObjectUtil.isEquals(Boolean.TRUE, null)); + assertFalse(ObjectUtil.isEquals(Boolean.TRUE, "true")); + assertTrue(ObjectUtil.isEquals(Boolean.TRUE, Boolean.TRUE)); + assertFalse(ObjectUtil.isEquals(Boolean.TRUE, Boolean.FALSE)); + + Object[] oa = new Object[] { new MyObject(), new MyObject() }; + int[] ia = new int[] { 1, 2, 3 }; + long[] la = new long[] { 1, 2, 3 }; + short[] sa = new short[] { 1, 2, 3 }; + byte[] ba = new byte[] { 1, 2, 3 }; + double[] da = new double[] { 1, 2, 3 }; + float[] fa = new float[] { 1, 2, 3 }; + boolean[] bla = new boolean[] { true, false, true }; + char[] ca = new char[] { 'a', 'b', 'c' }; + Object[] combo = { oa, ia, la, sa, ba, da, fa, bla, ca, null }; + + assertObjectEquals(oa); + assertObjectEquals(ia); + assertObjectEquals(la); + assertObjectEquals(sa); + assertObjectEquals(ba); + assertObjectEquals(da); + assertObjectEquals(fa); + assertObjectEquals(bla); + assertObjectEquals(ca); + assertObjectEquals(combo); + } + + private class MyObject { + @Override + public int hashCode() { + return 123; + } + } + + private void assertObjectEquals(Object array) throws Exception { + Method clone = Object.class.getDeclaredMethod("clone"); + clone.setAccessible(true); + + assertTrue(ObjectUtil.isEquals(array, array)); // same + assertTrue(ObjectUtil.isEquals(array, clone.invoke(array))); // equals to copy + + Object copy = Array.newInstance(array.getClass().getComponentType(), Array.getLength(array) - 1); + System.arraycopy(array, 0, copy, 0, Array.getLength(copy)); + + assertFalse(ObjectUtil.isEquals(array, copy)); // not equals + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/PropertiesTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/PropertiesTest.java new file mode 100644 index 000000000..dbe81bac3 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/PropertiesTest.java @@ -0,0 +1,45 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +public class PropertiesTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(PropertiesSampeBean.class); + + @Test + public void testToStringEqualsAndHashCode() { + Properties subject = classDescriptor.getProperties(); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class PropertiesSampeBean { + + private String foo; + private String bar; + + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/PropertyDescriptorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/PropertyDescriptorTest.java new file mode 100644 index 000000000..758285ad9 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/PropertyDescriptorTest.java @@ -0,0 +1,129 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.Optional; + +import javax.persistence.Version; + +import org.junit.Test; + +import lombok.Data; + + +public class PropertyDescriptorTest { + + private static ClassIntrospector classIntrospector = ClassIntrospector.builder() + .withIncludeFieldsAsProperties(true) + .withEnhancedProperties(true) + .withScanAccessible(true) + .withScanStatics(false) + .build(); + + // given + private ClassDescriptor classDescriptor = classIntrospector.introspect(SampleBean.class); + + @Test + public void testToString() { + PropertyDescriptor subject = classDescriptor.getPropertyDescriptor("name", true); + + // then + assertThatCode(() -> { + subject.toString(); + subject.hashCode(); + subject.equals(subject); + }).doesNotThrowAnyException(); + } + + @Test + public void testGetField() { + // then + assertThat(classDescriptor.getPropertyDescriptor("name", true) + .getField()) + .isNotNull(); + + } + + @Test + public void testGetGetter() { + // then + assertThat(classDescriptor.getPropertyDescriptor("name", true) + .getGetter(true)) + .isNotNull(); + + } + + @Test + public void testGetSetter() { + // then + assertThat(classDescriptor.getPropertyDescriptor("name", true) + .getSetter(true)) + .isNotNull(); + + } + + @Test + public void testResolveKeyType() { + // then + assertThat(classDescriptor.getPropertyDescriptor("optional", true) + .resolveKeyType(true)) + .isEqualTo(String.class); + + } + + @Test + public void testResolveComponentType() { + // then + assertThat(classDescriptor.getPropertyDescriptor("optional", true) + .resolveComponentType(true)) + .isEqualTo(String.class); + + } + + @Test + public void testIsFieldOnlyDescriptor() { + // then + assertThat(classDescriptor.getPropertyDescriptor("version", true) + .isFieldOnlyDescriptor()) + .isFalse(); + + } + + @Test + public void testGetAnnotations() { + // then + assertThat(classDescriptor.getPropertyDescriptor("version", true) + .getAnnotations()) + .isNotNull(); + + } + + @Test + public void testGetAnnotationDescriptor() { + // then + assertThat(classDescriptor.getPropertyDescriptor("version", true) + .getAnnotationDescriptor(Version.class)) + .isNotNull(); + + } + + @Test + public void testGetAnnotation() { + // then + assertThat(classDescriptor.getPropertyDescriptor("version", true) + .getAnnotation(Version.class)) + .isNotNull(); + + } + + @Data + static class SampleBean { + private final String id; + @Version + private Integer version; + private String name; + private Optional optional; + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ReflectionUtilTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ReflectionUtilTest.java new file mode 100644 index 000000000..19fba6b81 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/introspection/ReflectionUtilTest.java @@ -0,0 +1,344 @@ +package com.introproventures.graphql.jpa.query.introspection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import javax.management.loading.MLet; + +import org.junit.Test; + + +public class ReflectionUtilTest { + + @Test + public void getAllMethodsOfClass() { + assertNull(ReflectionUtil.getAllMethodsOfClass(null)); + + Method[] methods = ReflectionUtil.getAllMethodsOfClass(MLet.class); + assertTrue(methods.length > 0); + + Method equalsMethod = ReflectionUtil.getMethod(Object.class, "equals", Object.class); + + assertTrue(methods.length > 0); + List methodList = Arrays.asList(methods); + + assertFalse(methodList.contains(equalsMethod)); + + List> list = ClassUtil.getAllInterfaces(MLet.class); + + int interMethodLength = 0; + for (Class clazz : list) { + Method[] interMethods = ReflectionUtil.getAllMethodsOfClass(clazz); + interMethodLength += interMethods.length; + } + + assertTrue(methods.length > interMethodLength); + } + + @Test + public void getAllFieldsOfClass() { + assertNull(ReflectionUtil.getAllFieldsOfClass(null)); + assertNull(ReflectionUtil.getAllFieldsOfClass(Object.class)); + + assertEquals(0, ReflectionUtil.getAllFieldsOfClass(List.class).length); + Field[] fields = ReflectionUtil.getAllFieldsOfClass(String.class); + assertTrue(fields.length > 0); + + Field[] instancefields = ReflectionUtil.getAllInstanceFields(String.class); + assertTrue(instancefields.length > 0); + + assertTrue(fields.length - instancefields.length > 0); + } + + @Test + public void getComponentType() throws Exception { + Field f1 = BaseClass.class.getField("f1"); + Field f5 = ConcreteClass.class.getField("f5"); + + assertNull(ReflectionUtil.getComponentType(f1.getGenericType())); + assertEquals(Long.class, ReflectionUtil.getComponentType(f5.getGenericType())); + } + + @Test + public void getAnnotationMethods() { + assertNull(ReflectionUtil.getAnnotationMethods((Class) null, (Class) null)); + + assertNull(ReflectionUtil.getAnnotationMethods((Class) null, AnnotationClass.TestAnnotation.class)); + assertNull(ReflectionUtil.getAnnotationMethods(AnnotationClass.class, (Class) null)); + + List list = + ReflectionUtil.getAnnotationMethods(AnnotationClass.class, AnnotationClass.TestAnnotation.class); + + assertTrue(list.size() == 8); + + list = ReflectionUtil.getAnnotationMethods(AnnotationClass.class, Test.class); + + assertTrue(list.size() == 0); + } + + @Test + public void getAnnotationFields() { + assertNull(ReflectionUtil.getAnnotationFields((Class) null, (Class) null)); + + assertNull(ReflectionUtil.getAnnotationFields((Class) null, AnnotationClass.TestAnnotation.class)); + assertNull(ReflectionUtil.getAnnotationFields(AnnotationClass.class, (Class) null)); + + Field[] fields = + ReflectionUtil.getAnnotationFields(AnnotationClass.class, AnnotationClass.TestAnnotation.class); + + assertTrue(fields.length == 2); + + fields = ReflectionUtil.getAnnotationFields(AnnotationClass.class, Test.class); + + assertTrue(ArrayUtil.isEmpty(fields)); + + } + + @Test + public void getGenericSuperType() throws Exception { + Class[] genericSupertypes = ReflectionUtil.getGenericSuperTypes(ConcreteClass.class); + assertEquals(String.class, genericSupertypes[0]); + assertEquals(Integer.class, genericSupertypes[1]); + } + + @Test + public void getRawType() throws Exception { + Field f1 = BaseClass.class.getField("f1"); + Field f2 = BaseClass.class.getField("f2"); + Field f3 = BaseClass.class.getField("f3"); + Field f4 = ConcreteClass.class.getField("f4"); + Field f5 = ConcreteClass.class.getField("f5"); + Field array1 = BaseClass.class.getField("array1"); + + assertEquals(String.class, ReflectionUtil.getRawType(f1.getGenericType(), ConcreteClass.class)); + assertEquals(Integer.class, ReflectionUtil.getRawType(f2.getGenericType(), ConcreteClass.class)); + assertEquals(String.class, ReflectionUtil.getRawType(f3.getGenericType(), ConcreteClass.class)); + assertEquals(Long.class, ReflectionUtil.getRawType(f4.getGenericType(), ConcreteClass.class)); + assertEquals(List.class, ReflectionUtil.getRawType(f5.getGenericType(), ConcreteClass.class)); + assertEquals(String[].class, ReflectionUtil.getRawType(array1.getGenericType(), ConcreteClass.class)); + + assertEquals(Object.class, ReflectionUtil.getRawType(f1.getGenericType())); + } + + @Test + public void invokeMethod() { + assertNull(ReflectionUtil.invokeMethod(null, (Object) null)); + assertNull(ReflectionUtil.invokeMethod(null, new Object(), new Object())); + assertNull(ReflectionUtil.invokeMethod(null, new Object())); + + assertNull(ReflectionUtil.invokeMethod((Object) null, (String) null, (Object[]) null)); + assertNull(ReflectionUtil.invokeMethod((Object) null, (String) null, (Class[]) null, (Object) null)); + assertNull(ReflectionUtil.invokeMethod("", (String) null, (Class[]) null, (Object) null)); + assertNull(ReflectionUtil.invokeMethod((Object) null, (String) null, (Class[]) null, new Object[] {})); + assertNull(ReflectionUtil.invokeMethod((Object) null, (String) null, new Class[0], (Object[]) null)); + + assertNull(ReflectionUtil.invokeMethod(null, new Object(), new Object())); + assertNull(ReflectionUtil.invokeMethod(null, new Object())); + + Method method = null; + try { + method = String.class.getMethod("valueOf", int.class); + assertEquals("1", ReflectionUtil.invokeMethod(method, (Object) null, 1)); + assertEquals("1", ReflectionUtil.invokeMethod(method, (Object) "", 1)); + assertEquals("1", ReflectionUtil.invokeMethod(method, new Object(), 1)); + + method = String.class.getMethod("trim"); + assertEquals("xxx", ReflectionUtil.invokeMethod(method, (Object) " xxx ")); + assertEquals("xxx", ReflectionUtil.invokeMethod(method, new Object())); + + } catch (Exception e) { + assertTrue(e instanceof RuntimeException); + } + + List list = new ArrayList<>(); + + try { + method = ArrayList.class.getDeclaredMethod("RangeCheck", int.class); + ReflectionUtil.invokeMethod(method, list, Integer.MAX_VALUE); + } catch (Exception e) { + InvocationTargetException ex = (InvocationTargetException) e.getCause(); + + if (ex != null) { + assertTrue(ex.getTargetException() instanceof IndexOutOfBoundsException); + } + } + + try { + + assertEquals("xxx", ReflectionUtil.invokeMethod(" xxx ", "trim", null, (Object[]) null)); + assertEquals("xxx", ReflectionUtil.invokeMethod(new Object(), "trim", null, (Object[]) null)); + + } catch (Exception e) { + assertTrue(e instanceof RuntimeException); + } + + list = new ArrayList<>(); + + try { + ReflectionUtil.invokeMethod(list, "RangeCheck", new Class[] { int.class }, Integer.MAX_VALUE); + } catch (Exception e) { + + if (e.getCause() instanceof NoSuchMethodException) { + + } else { + + InvocationTargetException ex = (InvocationTargetException) e.getCause(); + + assertTrue(ex.getTargetException() instanceof IndexOutOfBoundsException); + } + } + + } + public static class BaseClass { + public A f1; + public B f2; + public String f3; + public A[] array1; + } + + public static class ConcreteClass extends BaseClass { + public Long f4; + public List f5; + } + + public static class BaseClass2 extends BaseClass { + } + + public static class ConcreteClass2 extends BaseClass2 { + } + + public static class Soo { + public List stringList; + public String[] strings; + public String string; + + public List getIntegerList() { + return null; + } + + public Integer[] getIntegers() { + return null; + } + + public Integer getInteger() { + return null; + } + + public T getTemplate(T foo) { + return null; + } + + public Collection getCollection() { + return null; + } + + public Collection getCollection2() { + return null; + } + } + + public interface SomeGuy { + } + + public interface Cool extends SomeGuy { + } + + public interface Vigilante { + } + + public interface Flying extends Vigilante { + } + + public interface SuperMario extends Flying, Cool { + }; + + public class User implements SomeGuy { + } + + public class SuperUser extends User implements Cool { + } + + public class SuperMan extends SuperUser implements Flying { + } + + public static class AnnotationClass { + + private int x; + @TestAnnotation(value = "y") + private int y; + + private String z; + @TestAnnotation(value = "d") + private Date d; + + public int getX() { + return x; + } + + @TestAnnotation(value = "setX") + public void setX(int x) { + this.x = x; + } + + @TestAnnotation(value = "getY") + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + @TestAnnotation(value = "getZ") + public String getZ() { + return z; + } + + @TestAnnotation(value = "setZ") + public void setZ(String z) { + this.z = z; + } + + @TestAnnotation(value = "getD") + public Date getD() { + return d; + } + + @TestAnnotation(value = "setD") + public void setD(Date d) { + this.d = d; + } + + @Override + @TestAnnotation(value = "toString") + public String toString() { + return super.toString(); + } + + @Override + @TestAnnotation(value = "clone") + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface TestAnnotation { + String value(); + } + + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/localdatetime/GraphQLLocalDateTimeTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/localdatetime/GraphQLLocalDateTimeTest.java new file mode 100644 index 000000000..928377b32 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/localdatetime/GraphQLLocalDateTimeTest.java @@ -0,0 +1,1201 @@ +package com.introproventures.graphql.jpa.query.localdatetime; + +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, + properties = "spring.datasource.data=LocalDatetTmeData.sql") +@TestPropertySource({"classpath:hibernate.properties"}) +public class GraphQLLocalDateTimeTest { + + @SpringBootApplication + static class Application { + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + return new GraphQLJpaSchemaBuilder(entityManager) + .name("CustomAttributeConverterSchema") + .description("Custom Attribute Converter Schema"); + } + } + + @Autowired + private GraphQLExecutor executor; + + @Autowired + private EntityManager entityManager; + + @Test + public void queryLocalDateWithEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " EQ:\"2019-08-06\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=6, localDate=2019-08-06, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " BETWEEN:[\"2019-08-05\",\"2019-08-06\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=5, localDate=2019-08-05, description=Add test for LocalDate.}, " + + "{id=6, localDate=2019-08-06, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithBetweenDuplicateDateTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " BETWEEN:[\"2019-08-05\",\"2019-08-05\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=5, localDate=2019-08-05, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithGreaterThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " GE:\"2019-08-06\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=6, localDate=2019-08-06, description=Add test for LocalDate.}, " + + "{id=7, localDate=2019-08-07, description=Add test for LocalDate.}, " + + "{id=8, localDate=2019-08-08, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithGreaterThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " GT:\"2019-08-05\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=6, localDate=2019-08-06, description=Add test for LocalDate.}, " + + "{id=7, localDate=2019-08-07, description=Add test for LocalDate.}, " + + "{id=8, localDate=2019-08-08, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithNotBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " NOT_BETWEEN:[\"2019-08-04\",\"2019-08-05\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=1, localDate=2019-08-01, description=Add test for LocalDate.}, " + + "{id=2, localDate=2019-08-02, description=Add test for LocalDate.}, " + + "{id=3, localDate=2019-08-03, description=Add test for LocalDate.}, " + + "{id=6, localDate=2019-08-06, description=Add test for LocalDate.}, " + + "{id=7, localDate=2019-08-07, description=Add test for LocalDate.}, " + + "{id=8, localDate=2019-08-08, description=Add test for LocalDate.}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithNotEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " NE:\"2019-08-04\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=1, localDate=2019-08-01, description=Add test for LocalDate.}, " + + "{id=2, localDate=2019-08-02, description=Add test for LocalDate.}, " + + "{id=3, localDate=2019-08-03, description=Add test for LocalDate.}, " + + "{id=5, localDate=2019-08-05, description=Add test for LocalDate.}, " + + "{id=6, localDate=2019-08-06, description=Add test for LocalDate.}, " + + "{id=7, localDate=2019-08-07, description=Add test for LocalDate.}, " + + "{id=8, localDate=2019-08-08, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithLessThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " LE:\"2019-08-06\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=1, localDate=2019-08-01, description=Add test for LocalDate.}, " + + "{id=2, localDate=2019-08-02, description=Add test for LocalDate.}, " + + "{id=3, localDate=2019-08-03, description=Add test for LocalDate.}, " + + "{id=4, localDate=2019-08-04, description=Add test for LocalDate.}, " + + "{id=5, localDate=2019-08-05, description=Add test for LocalDate.}, " + + "{id=6, localDate=2019-08-06, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateWithLessThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDate:{" + + " LT:\"2019-08-02\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDate" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, localDate=2019-08-01, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithGreaterThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " GT:\"2019-08-06T07:00:00.00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=6, localDateTime=2019-08-06T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=7, localDateTime=2019-08-07T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=8, localDateTime=2019-08-08T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithLessThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " LT:\"2019-08-02T07:00:00.00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, localDateTime=2019-08-01T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " EQ:\"2019-08-06T10:58:08.389991\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=6, localDateTime=2019-08-06T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " BETWEEN:[\"2019-08-05T10:58:08.389991\",\"2019-08-06T13:58:08.389991\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=5, localDateTime=2019-08-05T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=6, localDateTime=2019-08-06T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithLessThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " LE:\"2019-08-02T07:00:00.00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, localDateTime=2019-08-01T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithNotBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " NOT_BETWEEN:[\"2019-08-02T10:58:08.389991\",\"2019-08-08T13:58:08.389991\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, localDateTime=2019-08-01T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryLocalDateTimeWithNotEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " localDateTime:{" + + " NE:\"2019-08-05T10:58:08.389991\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " localDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=1, localDateTime=2019-08-01T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=2, localDateTime=2019-08-02T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=3, localDateTime=2019-08-03T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=4, localDateTime=2019-08-04T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=6, localDateTime=2019-08-06T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=7, localDateTime=2019-08-07T10:58:08.389991, description=Add test for LocalDate.}, " + + "{id=8, localDateTime=2019-08-08T10:58:08.389991, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithGreaterOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " GE:\"2019-08-08T10:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=8, offsetDateTime=2019-08-08T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithGreaterThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " GT:\"2019-08-07T10:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=8, offsetDateTime=2019-08-08T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " BETWEEN:[\"2019-08-06T09:58:07.915991+07:00\",\"2019-08-06T15:58:07.915991+07:00\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=6, offsetDateTime=2019-08-06T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithNotBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " NOT_BETWEEN:[\"2019-08-02T10:58:07.915991+07:00\",\"2019-08-08T15:58:07.915991+07:00\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, offsetDateTime=2019-08-01T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithLessThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " LT:\"2019-08-02T10:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, offsetDateTime=2019-08-01T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithLessThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " LE:\"2019-08-01T15:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, offsetDateTime=2019-08-01T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryOffsetDateTimeWithNotEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " offsetDateTime:{" + + " NE:\"2019-08-01T10:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " offsetDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=2, offsetDateTime=2019-08-02T03:58:07.915991Z, description=Add test for LocalDate.}, " + + "{id=3, offsetDateTime=2019-08-03T03:58:07.915991Z, description=Add test for LocalDate.}, " + + "{id=4, offsetDateTime=2019-08-04T03:58:07.915991Z, description=Add test for LocalDate.}, " + + "{id=5, offsetDateTime=2019-08-05T03:58:07.915991Z, description=Add test for LocalDate.}, " + + "{id=6, offsetDateTime=2019-08-06T03:58:07.915991Z, description=Add test for LocalDate.}, " + + "{id=7, offsetDateTime=2019-08-07T03:58:07.915991Z, description=Add test for LocalDate.}, " + + "{id=8, offsetDateTime=2019-08-08T03:58:07.915991Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithGreaterThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " GT:\"2019-08-07T19:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=8, zonedDateTime=2019-08-08T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithGreaterThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " GE:\"2019-08-08T10:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=8, zonedDateTime=2019-08-08T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithLessThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " LE:\"2019-08-01T03:58:08.153992Z[UTC]\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, zonedDateTime=2019-08-01T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithLessThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " LT:\"2019-08-02T10:58:07.915991+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, zonedDateTime=2019-08-01T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " BETWEEN:[\"2019-08-05T10:58:07.915991+07:00\",\"2019-08-06T15:58:07.915991+07:00\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=5, zonedDateTime=2019-08-05T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=6, zonedDateTime=2019-08-06T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithNotBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " NOT_BETWEEN:[\"2019-08-02T10:58:07.915991+07:00\",\"2019-08-08T15:58:07.915991+07:00\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, zonedDateTime=2019-08-01T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithNotEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " NE:\"2019-08-05T03:58:08.153992Z[UTC]\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=1, zonedDateTime=2019-08-01T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=2, zonedDateTime=2019-08-02T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=3, zonedDateTime=2019-08-03T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=4, zonedDateTime=2019-08-04T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=6, zonedDateTime=2019-08-06T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=7, zonedDateTime=2019-08-07T03:58:08.153992Z[UTC], description=Add test for LocalDate.}, " + + "{id=8, zonedDateTime=2019-08-08T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryZonedDateTimeWithEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " zonedDateTime:{" + + " EQ:\"2019-08-06T10:58:08.153992+07:00\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " zonedDateTime" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=6, zonedDateTime=2019-08-06T03:58:08.153992Z[UTC], description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithGreaterThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " GT:\"2019-08-07T03:58:08.842270Z\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=8, instant=2019-08-08T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithGreaterThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " GE:\"2019-08-08T03:58:08.842270Z\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=8, instant=2019-08-08T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithLessThanOrEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " LE:\"2019-08-01T03:58:08.842270Z\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, instant=2019-08-01T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithLessThanTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " LT:\"2019-08-02T03:58:08.842270Z\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, instant=2019-08-01T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " BETWEEN:[\"2019-08-06T03:58:08.842270Z\",\"2019-08-06T03:58:08.842270Z\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=6, instant=2019-08-06T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithNotBetweenTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " NOT_BETWEEN:[\"2019-08-02T03:58:08.842270Z\",\"2019-08-08T03:58:08.842270Z\"]" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=1, instant=2019-08-01T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithNotEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " NE:\"2019-08-08T03:58:08.842270Z\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[" + + "{id=1, instant=2019-08-01T03:58:08.842270Z, description=Add test for LocalDate.}, " + + "{id=2, instant=2019-08-02T03:58:08.842270Z, description=Add test for LocalDate.}, " + + "{id=3, instant=2019-08-03T03:58:08.842270Z, description=Add test for LocalDate.}, " + + "{id=4, instant=2019-08-04T03:58:08.842270Z, description=Add test for LocalDate.}, " + + "{id=5, instant=2019-08-05T03:58:08.842270Z, description=Add test for LocalDate.}, " + + "{id=6, instant=2019-08-06T03:58:08.842270Z, description=Add test for LocalDate.}, " + + "{id=7, instant=2019-08-07T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryInstantWithEqualTest() { + //given + String query = "query{" + + " localDates" + + " (where:{" + + " instant:{" + + " EQ:\"2019-08-06T03:58:08.842270Z\"" + + " }" + + " })" + + "{" + + " select{" + + " id" + + " instant" + + " description" + + " }" + + " }" + + "}"; + + String expected = "{localDates={select=[{id=6, instant=2019-08-06T03:58:08.842270Z, description=Add test for LocalDate.}]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/localdatetime/model/LocalDateEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/localdatetime/model/LocalDateEntity.java new file mode 100644 index 000000000..48b7b344d --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/localdatetime/model/LocalDateEntity.java @@ -0,0 +1,33 @@ +package com.introproventures.graphql.jpa.query.localdatetime.model; + +import lombok.Getter; + +import javax.persistence.*; +import java.time.*; + +@Table(name = "LOCAL_DATE") +@Entity(name = "localDate") +@Getter +public class LocalDateEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "LOCALDATE") + LocalDate localDate; + + @Column(name = "LOCALDATETIME") + LocalDateTime localDateTime; + + @Column(name = "OFFSETDATETIME") + OffsetDateTime offsetDateTime; + + @Column(name = "ZONEDDATETIME") + ZonedDateTime zonedDateTime; + + @Column(name = "INSTANT") + Instant instant; + + @Column(name = "description") + String description; +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/BooksSchemaBuildTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/BooksSchemaBuildTest.java index fdf8c977b..94c88697f 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/BooksSchemaBuildTest.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/BooksSchemaBuildTest.java @@ -17,13 +17,16 @@ package com.introproventures.graphql.jpa.query.schema; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenCode; -import java.util.stream.Collectors; +import java.util.Optional; import javax.persistence.EntityManager; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLList; import graphql.schema.GraphQLSchema; import org.junit.Before; import org.junit.Test; @@ -99,18 +102,13 @@ public void correctlyDerivesToManyOptionalFromGivenEntities() { .isNotNull(); //then - assertThat(schema.getQueryType() - .getFieldDefinition("Book") - .getType() - .getChildren() - .stream() - .map(GraphQLFieldDefinition.class::cast) - .collect(Collectors.toList()) - ) - .filteredOn("name", "author") - .extracting(it -> it.getArgument("optional")) - .extractingResultOf("getDefaultValue", Object.class) - .containsExactly(new Boolean(false)); + assertThat(getFieldForType("author", + "Book", + schema)) + .isPresent().get() + .extracting(it -> it.getArgument("optional")) + .extracting("defaultValue") + .containsExactly(Boolean.FALSE); } @Test @@ -124,20 +122,45 @@ public void correctlyDerivesToOneOptionalFromGivenEntities() { .isNotNull(); //then - assertThat(schema.getQueryType() - .getFieldDefinition("Author") - .getType() - .getChildren() - .stream() - .map(GraphQLFieldDefinition.class::cast) - .collect(Collectors.toList()) - ) - .filteredOn("name", "books") - .extracting(it -> it.getArgument("optional")) - .extractingResultOf("getDefaultValue", Object.class) - .containsExactly(new Boolean(true)); + assertThat(getFieldForType("books", + "Author", + schema)) + .isPresent().get() + .extracting(it -> it.getArgument("optional")) + .extracting("defaultValue") + .containsExactly(Boolean.TRUE); } - + + @Test + public void shouldBuildSchemaWithStringArrayAsStringListType() { + //given + //there is a property in the model that is of array type + + //when + GraphQLSchema schema = builder.build(); + + //then + Optional tags = getFieldForType("tags", + "SuperBook", + schema); + then(tags) + .isPresent().get() + .extracting(GraphQLFieldDefinition::getType) + .isInstanceOf(GraphQLList.class) + .extracting("wrappedType") + .extracting("name") + .containsOnly("String"); + } + + @Test + public void shouldBuildSchemaWithStringArrayAsStringListTypeWithoutAnyError() { + //given + //there is a property in the model that is of array type + + //then + thenCode(() -> builder.build()).doesNotThrowAnyException(); + } + @Test public void testBuildSchema(){ //given @@ -146,5 +169,18 @@ public void testBuildSchema(){ //then assertThat(schema).isNotNull(); } - + + private Optional getFieldForType(String fieldName, + String type, + GraphQLSchema schema) { + return schema.getQueryType() + .getFieldDefinition(type) + .getType() + .getChildren() + .stream() + .map(GraphQLFieldDefinition.class::cast) + .filter(graphQLFieldDefinition -> graphQLFieldDefinition.getName().equals(fieldName)) + .findFirst(); + } + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java index c63167e7b..1f390a590 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java @@ -1,9 +1,11 @@ package com.introproventures.graphql.jpa.query.schema; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.util.Lists.list; -import java.util.Arrays; -import java.util.List; +import java.util.Optional; import javax.persistence.EntityManager; @@ -20,9 +22,10 @@ import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; -import graphql.ErrorType; -import graphql.GraphQLError; -import graphql.validation.ValidationError; +import graphql.ExecutionResult; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLSchema; +import graphql.validation.ValidationErrorType; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE) @@ -48,6 +51,9 @@ public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManag @Autowired private GraphQLExecutor executor; + @Autowired + private GraphQLSchemaBuilder schemaBuilder; + @Test public void contextLoads() { Assert.isAssignable(GraphQLExecutor.class, executor.getClass()); @@ -69,17 +75,90 @@ public void getAllRecords() { @Test public void testIgnoreFields() { - String query = "query GraphQLCalcFields { CalculatedEntities { select {id title fieldMem fieldFun logic customLogic hideField hideFieldFunction } } }"; + String query = "" + + "query GraphQLCalcFields { " + + " CalculatedEntities { " + + " select {" + + " id" + + " title" + + " fieldMem" + + " fieldFun" + + " logic" + + " age" + + " customLogic" + + " hideField" + + " hideFieldFunction" + + " propertyIgnoredOnGetter" + + " ignoredTransientValue" + + " transientModifier" + + " transientModifierGraphQLIgnore" + + " parentField" + + " parentTransientModifier" + + " parentTransient" + + " parentTransientGetter" + + " parentGraphQLIngore" + + " parentGraphQLIgnoreGetter" + + " parentTransientGraphQLIgnore" + + " parentTransientModifierGraphQLIgnore" + + " parentTransientGraphQLIgnoreGetter" + + " Uppercase" + + " UppercaseGetter" + + " UppercaseGetterIgnore" + + " protectedGetter" + + " } " + + " } " + + "}"; + + //when + ExecutionResult result = executor.execute(query); + + //then + assertThat(result.getErrors()) + .isNotEmpty() + .extracting("validationErrorType", "queryPath") + .containsOnly( + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "hideField")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "hideFieldFunction")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "propertyIgnoredOnGetter")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "ignoredTransientValue")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentGraphQLIngore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentGraphQLIgnoreGetter")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientModifierGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "parentTransientGraphQLIgnoreGetter")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "transientModifierGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "transientModifierGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "UppercaseGetterIgnore")) + ); + } + @Test + public void shouldInheritMethodDescriptionFromBaseClass() { //when - List result = executor.execute(query).getErrors(); + GraphQLSchema schema = schemaBuilder.build(); //then - assertThat(result).hasSize(1); - assertThat(result.get(0)).isExactlyInstanceOf(ValidationError.class) - .extracting(ValidationError.class::cast) - .extracting("errorType", "queryPath") - .contains(ErrorType.ValidationError, Arrays.asList("CalculatedEntities", "select", "hideFieldFunction")); + Optional field = getFieldForType("parentTransientGetter", + "CalculatedEntity", + schema); + then(field) + .isPresent().get() + .extracting("description") + .isNotNull() + .containsExactly("getParentTransientGetter"); + } + + private Optional getFieldForType(String fieldName, + String type, + GraphQLSchema schema) { + return schema.getQueryType() + .getFieldDefinition(type) + .getType() + .getChildren() + .stream() + .map(GraphQLFieldDefinition.class::cast) + .filter(graphQLFieldDefinition -> graphQLFieldDefinition.getName().equals(fieldName)) + .findFirst(); } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/EntityIntrospectorTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/EntityIntrospectorTest.java new file mode 100644 index 000000000..ba2fee12e --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/EntityIntrospectorTest.java @@ -0,0 +1,403 @@ +package com.introproventures.graphql.jpa.query.schema; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.when; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.ManagedType; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector; +import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult; +import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; +import com.introproventures.graphql.jpa.query.schema.model.calculated.CalculatedEntity; +import com.introproventures.graphql.jpa.query.schema.model.calculated.ParentCalculatedEntity; +import com.introproventures.graphql.jpa.query.schema.model.metamodel.ClassWithCustomMetamodel; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE) +@TestPropertySource({"classpath:hibernate.properties"}) +public class EntityIntrospectorTest { + + @SpringBootApplication + static class Application { + } + + @Autowired + private EntityManager entityManager; + + // given + private final Class entityClass = CalculatedEntity.class; + + private EntityIntrospectionResult subject; + + @Before + public void setUp() { + ManagedType entityType = entityManager.getMetamodel() + .managedType(entityClass); + + this.subject = EntityIntrospector.introspect(entityType); + } + + + @Test(expected = NoSuchElementException.class) + public void testResultOfNoSuchElementException() throws Exception { + // then + EntityIntrospector.resultOf(Object.class); + } + + @Test(expected = NoSuchElementException.class) + public void testIsTransientNonExisting() throws Exception { + // then + assertThat(subject.isTransient("notFound")).isFalse(); + } + + @Test(expected = NoSuchElementException.class) + public void testIsIgnoredNonExisting() throws Exception { + // then + assertThat(subject.isIgnored("notFound")).isFalse(); + } + + @Test + public void shouldExcludeClassPropertyDescriptor() throws Exception { + // then + assertThat(subject.getPropertyDescriptor("class")).isEmpty(); + } + + @Test + public void testIsTransientFunction() throws Exception { + // then + assertThat(subject.isTransient("fieldFun")).isTrue(); + assertThat(subject.isTransient("hideFieldFunction")).isTrue(); + } + + @Test + public void testIsPersistentFunction() throws Exception { + // then + assertThat(subject.isPersistent("fieldFun")).isFalse(); + assertThat(subject.isPersistent("hideFieldFunction")).isFalse(); + } + + @Test + public void testIsTransientFields() throws Exception { + // then + assertThat(subject.isTransient("fieldFun")).isTrue(); + assertThat(subject.isTransient("fieldMem")).isTrue(); + assertThat(subject.isTransient("hideField")).isTrue(); + assertThat(subject.isTransient("logic")).isTrue(); + assertThat(subject.isTransient("transientModifier")).isTrue(); + assertThat(subject.isTransient("parentTransientModifier")).isTrue(); + assertThat(subject.isTransient("parentTransient")).isTrue(); + assertThat(subject.isTransient("parentTransientGetter")).isTrue(); + } + + @Test + public void testNotTransientFields() throws Exception { + // then + assertThat(subject.isTransient("id")).isFalse(); + assertThat(subject.isTransient("info")).isFalse(); + assertThat(subject.isTransient("title")).isFalse(); + assertThat(subject.isTransient("parentField")).isFalse(); + } + + @Test + public void testByPassSetMethod() throws Exception { + // then + assertThat(subject.isTransient("something")).isTrue(); + } + + @Test + public void shouldIgnoreMethodsThatAreAnnotatedWithGraphQLIgnore() { + //then + assertThat(subject.isIgnored("propertyIgnoredOnGetter")).isTrue(); + assertThat(subject.isIgnored("ignoredTransientValue")).isTrue(); + assertThat(subject.isIgnored("hideField")).isTrue(); + assertThat(subject.isIgnored("parentGraphQLIgnore")).isTrue(); + + assertThat(subject.isIgnored("transientModifier")).isFalse(); + assertThat(subject.isIgnored("parentTransientModifier")).isFalse(); + assertThat(subject.isIgnored("parentTransient")).isFalse(); + assertThat(subject.isIgnored("parentTransientGetter")).isFalse(); + } + + @Test + public void shouldNotIgnoreMethodsThatAreNotAnnotatedWithGraphQLIgnore() { + //then + assertThat(subject.isNotIgnored("propertyIgnoredOnGetter")).isFalse(); + assertThat(subject.isNotIgnored("ignoredTransientValue")).isFalse(); + assertThat(subject.isNotIgnored("hideField")).isFalse(); + assertThat(subject.isNotIgnored("parentGraphQLIgnore")).isFalse(); + + assertThat(subject.isNotIgnored("transientModifier")).isTrue(); + assertThat(subject.isNotIgnored("parentTransientModifier")).isTrue(); + assertThat(subject.isNotIgnored("parentTransient")).isTrue(); + assertThat(subject.isNotIgnored("parentTransientGetter")).isTrue(); + } + + @SuppressWarnings("rawtypes") + @Test + public void shouldGetClassesInHierarchy() { + //when + Class[] result = EntityIntrospector.resultOf(entityClass) + .getClasses() + .toArray(Class[]::new); + + //then + assertThat(result).containsExactly(CalculatedEntity.class, + ParentCalculatedEntity.class, + Object.class); + } + + @Test + public void testGetPropertyDescriptorsSchemaDescription() throws Exception { + // when + EntityIntrospectionResult result = EntityIntrospector.resultOf(CalculatedEntity.class); + + // then + assertThat(result.getPropertyDescriptors()).extracting(AttributePropertyDescriptor::getSchemaDescription) + .filteredOn(Optional::isPresent) + .extracting(Optional::get) + .containsOnly("i desc function", + "getParentTransientGetter", + "UppercaseGetter", + "title", + "transientModifier", + "i desc member", + "parentTransientModifier", + "Uppercase", + "protectedGetter"); + } + + @Test + public void testGetPropertyDescriptorSchemaDescriptionByAttribute() throws Exception { + Attribute attribute = Mockito.mock(Attribute.class); + + when(attribute.getName()).thenReturn("title"); + + // when + Optional result = EntityIntrospector.resultOf(CalculatedEntity.class) + .getPropertyDescriptor(attribute); + // then + assertThat(result.isPresent()).isTrue(); + } + + @Test + public void testGetParentEntitySchemaDescription() throws Exception { + // when + EntityIntrospectionResult result = EntityIntrospector.resultOf(CalculatedEntity.class); + + // then + assertThat(result.getSchemaDescription()).contains("ParentCalculatedEntity description"); + assertThat(result.hasSchemaDescription()).isTrue(); + } + + @Test + public void testUppercasePropertyNamesAreSupported() throws Exception { + // when + EntityIntrospectionResult result = EntityIntrospector.resultOf(CalculatedEntity.class); + + // then + assertThat(result.getPropertyDescriptor("Uppercase")).isPresent(); + + assertThat(result.getPropertyDescriptor("Uppercase") + .get()) + .extracting(AttributePropertyDescriptor::isIgnored) + .isEqualTo(false); + + assertThat(result.getPropertyDescriptor("Uppercase") + .get()) + .extracting(AttributePropertyDescriptor::isTransient) + .isEqualTo(false); + + assertThat(result.getPropertyDescriptor("Uppercase") + .get() + .getSchemaDescription()) + .contains("Uppercase"); + + assertThat(result.getPropertyDescriptor("UppercaseGetter") + .get()) + .extracting(AttributePropertyDescriptor::isIgnored) + .isEqualTo(false); + + assertThat(result.getPropertyDescriptor("UppercaseGetter") + .get() + .getSchemaDescription()) + .contains("UppercaseGetter"); + + assertThat(result.getPropertyDescriptor("UppercaseGetter") + .get()) + .extracting(AttributePropertyDescriptor::isTransient) + .isEqualTo(false); + + assertThat(result.getPropertyDescriptor("uppercaseGetterIgnore") + .get()) + .extracting(AttributePropertyDescriptor::isIgnored) + .isEqualTo(true); + } + + @Test + public void testPrivateModifierOnGetterProperty() throws Exception { + // when + EntityIntrospectionResult result = EntityIntrospector.resultOf(CalculatedEntity.class); + + // then + assertThat(subject.isIgnored("age")).isFalse(); + assertThat(subject.isPersistent("age")).isTrue(); + assertThat(subject.isTransient("age")).isFalse(); + + assertThat(result.getPropertyDescriptor("age")).isPresent(); + assertThat(result.getPropertyDescriptor("age") + .get() + .getReadMethod()) + .isEmpty(); + } + + @Test + public void testProtectedModifierOnGetterProperty() throws Exception { + // when + EntityIntrospectionResult result = EntityIntrospector.resultOf(CalculatedEntity.class); + + // then + assertThat(subject.isIgnored("protectedGetter")).isFalse(); + assertThat(subject.isPersistent("protectedGetter")).isTrue(); + assertThat(subject.isTransient("protectedGetter")).isFalse(); + + assertThat(result.getPropertyDescriptor("protectedGetter")).isPresent(); + assertThat(result.getPropertyDescriptor("protectedGetter") + .get() + .getReadMethod()) + .isPresent(); + } + + @Test + public void shouldNotFailWhenPropertyIsDuplicatedInParentAndChild() { + // given + // There is a duplicated property in parent and child + + // then + assertThatCode(() -> EntityIntrospector.resultOf(CalculatedEntity.class)).doesNotThrowAnyException(); + } + + @Test + public void shouldCorrectlyIntrospectPropertyDuplicatedInParentAndChild() { + // given + // There is a duplicated property in parent and child + + // when + EntityIntrospectionResult introspectionResult = EntityIntrospector.resultOf(CalculatedEntity.class); + + // then + Optional propertyOverriddenInChild = introspectionResult.getPropertyDescriptor("propertyDuplicatedInChild"); + assertThat(propertyOverriddenInChild).isPresent(); + } + + @Test + public void testGetTransientPropertyDescriptors() { + // given + ManagedType managedType = entityManager.getMetamodel().entity(CalculatedEntity.class); + + // when + EntityIntrospectionResult result = EntityIntrospector.introspect(managedType); + + // then + assertThat(result.getTransientPropertyDescriptors()).extracting(AttributePropertyDescriptor::getName) + .containsOnly("fieldFun", + "fieldMem", + "hideField", + "logic", + "transientModifier", + "parentTransientModifier", + "parentTransient", + "parentTransientGetter", + "uppercaseGetterIgnore", + "hideFieldFunction", + "transientModifierGraphQLIgnore", + "customLogic", + "parentTransientModifierGraphQLIgnore", + "ignoredTransientValue", + "something", + "parentTransientGraphQLIgnore"); + ; + } + + @Test + public void testGetPersistentPropertyDescriptors() { + // given + ManagedType managedType = entityManager.getMetamodel().entity(CalculatedEntity.class); + + // when + EntityIntrospectionResult result = EntityIntrospector.introspect(managedType); + + // then + assertThat(result.getPersistentPropertyDescriptors()).extracting(AttributePropertyDescriptor::getName) + .containsOnly("Uppercase", + "title", + "parentGraphQLIgnore", + "parentGraphQLIgnoreGetter", + "propertyIgnoredOnGetter", + "id", + "info", + "parentTransientGraphQLIgnoreGetter", + "protectedGetter", + "parentField", + "UppercaseGetter", + "propertyDuplicatedInChild", + "age"); + } + + @Test + public void testGetIgnoredPropertyDescriptors() { + // given + ManagedType managedType = entityManager.getMetamodel().entity(CalculatedEntity.class); + + // when + EntityIntrospectionResult result = EntityIntrospector.introspect(managedType); + + // then + assertThat(result.getIgnoredPropertyDescriptors()).extracting(AttributePropertyDescriptor::getName) + .containsOnly("uppercaseGetterIgnore", + "hideFieldFunction", + "parentGraphQLIgnore", + "parentGraphQLIgnoreGetter", + "transientModifierGraphQLIgnore", + "propertyIgnoredOnGetter", + "parentTransientModifierGraphQLIgnore", + "ignoredTransientValue", + "hideField", + "parentTransientGraphQLIgnoreGetter", + "parentTransientGraphQLIgnore"); + } + + @Test + public void shouldIntrospectEntityWithCustomMetamodel() { + //given + EntityType entity = entityManager.getMetamodel() + .entity(ClassWithCustomMetamodel.class); + + //when + EntityIntrospectionResult result = EntityIntrospector.introspect(entity); + + //then + assertThat(result.getPropertyDescriptors()) + .extracting(AttributePropertyDescriptor::getName) + .containsOnly("id", + "publicValue", + "protectedValue", + "ignoredProtectedValue"); + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index 2d2b06907..b909ff962 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -17,20 +17,19 @@ package com.introproventures.graphql.jpa.query.schema; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.util.Lists.list; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.persistence.EntityManager; -import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; -import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; -import graphql.ErrorType; -import graphql.ExecutionResult; -import graphql.GraphQLError; -import graphql.validation.ValidationError; +import org.assertj.core.util.Maps; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -42,6 +41,15 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.Assert; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +import graphql.ErrorType; +import graphql.ExecutionResult; +import graphql.GraphQLError; +import graphql.validation.ValidationError; +import graphql.validation.ValidationErrorType; + @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment=WebEnvironment.NONE) @@ -619,7 +627,7 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitle() { " title: {LIKE: \"War\"}" + " }" + " }" + - " }) {" + + " }) {" + " select {" + " id" + " name" + @@ -827,8 +835,82 @@ public void queryForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + + @Test + public void queryTotalForAuthorsWithWhereEXISTSBooksLIKETitleEmpty() { + //given + String query = "query { " + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " author: {name: {LIKE: \"Anton\"}}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " total" + + " pages" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + + " }"+ + "}"; + + String expected = "{Authors={total=0, pages=0, select=[]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryTotalForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { + //given + String query = "query { " + + " Authors(where: {" + + " books: {" + + " NOT_EXISTS: {" + + " author: {" + + " name: {LIKE: \"Leo\"}" + + " }" + + " }" + + " }" + + " }) {" + + " total" + + " pages" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + + " }"+ + "}"; + + String expected = "{Authors={total=3, pages=1, select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=5, title=The Cherry Orchard}, " + + "{id=6, title=The Seagull}, " + + "{id=7, title=Three Sisters}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryForAuthorssWithWhereBooksGenreEquals() { //given @@ -1412,7 +1494,232 @@ public void queryForAuthorsWithExlicitOptionalBooksTrue() { //when Object result = executor.execute(query).getData(); + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryForTransientMethodAnnotatedWithGraphQLIgnoreShouldFail() { + //given + String query = "" + + "query { " + + " Books {" + + " select {" + + " authorName" + + " }" + + " }" + + "}"; + + //when + ExecutionResult result = executor.execute(query); + + // then + assertThat(result.getErrors()) + .isNotEmpty() + .extracting("validationErrorType", "queryPath") + .containsOnly(tuple(ValidationErrorType.FieldUndefined, list("Books", "select", "authorName"))); + } + + @Test + public void queryWithEQNotMatchingCase() { + //given: + String query = "query { Books ( where: { title: {EQ: \"War And Peace\"}}) { select { id title} } }"; + + String expected = "{Books={select=[]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithEQMatchingCase() { + //given: + String query = "query { Books ( where: { title: {EQ: \"War and Peace\"}}) { select { id title} } }"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriteriaExpression() { + //given: + String query = "query ($genre: Genre) {" + + " Authors(where: { " + + " books: {" + + " genre: {EQ: $genre}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + //when: 1st query + Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); + + String expected1 = "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + + "{id=6, title=The Seagull, genre=PLAY}, " + + "{id=7, title=Three Sisters, genre=PLAY}" + + "]}" + + "]}}"; + + //then: + assertThat(result1.toString()).isEqualTo(expected1); + + //when: 2nd query + Object result2 = executor.execute(query, Collections.singletonMap("genre", "NOVEL")).getData(); + + String expected2 = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}" + + "]}}"; + + //then: + assertThat(result2.toString()).isEqualTo(expected2); + } + + @Test + public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteriaExpression() { + //given: + String query = "query ($genre: Genre) {" + + " Authors {" + + " select {" + + " id" + + " name" + + " books(where:{ genre: {EQ: $genre} }) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + + "}"; + + //when: 1st query + Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); + + String expected1 = "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + + "{id=6, title=The Seagull, genre=PLAY}, " + + "{id=7, title=Three Sisters, genre=PLAY}" + + "]}" + + "]}}"; + + //then: + assertThat(result1.toString()).isEqualTo(expected1); + + //when: 2nd query + Object result2 = executor.execute(query, Collections.singletonMap("genre", "NOVEL")).getData(); + + String expected2 = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}" + + "]}}"; + + //then: + assertThat(result2.toString()).isEqualTo(expected2); + } + + @Test + public void queryWithEnumParameterShouldExecuteWithNoError() { + //given + String query = "" + + "query($orderById: OrderBy) {" + + " Books {" + + " select {" + + " id(orderBy: $orderById)" + + " title" + + " }" + + " }" + + "}"; + Map variables = Maps.newHashMap("orderById", + "DESC"); + + //when + ExecutionResult executionResult = executor.execute(query, + variables); + + // then + List errors = executionResult.getErrors(); + Map data = executionResult.getData(); + then(errors).isEmpty(); + then(data) + .isNotNull().isNotEmpty() + .extracting("Books") + .flatExtracting("select") + .extracting("id", "title") + .containsExactly( + tuple(7L, + "Three Sisters"), + tuple(6L, + "The Seagull"), + tuple(5L, + "The Cherry Orchard"), + tuple(3L, + "Anna Karenina"), + tuple(2L, + "War and Peace") + ); + } + + // https://github.com/introproventures/graphql-jpa-query/issues/198 + @Test + public void queryOptionalElementCollections() { + //given + String query = "{ Author(id: 8) { id name phoneNumbers books { id title tags } } }"; + + String expected = "{Author={id=8, name=Igor Dianov, phoneNumbers=[], books=[]}}"; + + //when + Object result = executor.execute(query).getData(); + // then assertThat(result.toString()).isEqualTo(expected); } + + @Test + public void queryElementCollectionsWithWhereCriteriaExpression() { + //given: + String query = "query {" + + " Books(where: {tags: {EQ: \"war\"}}) {" + + " select {" + + " id" + + " title" + + " tags" + + " }" + + " }" + + "}"; + + String expected = "{Books={select=[{id=2, title=War and Peace, tags=[piece, war]}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java new file mode 100644 index 000000000..b5ee97df3 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java @@ -0,0 +1,395 @@ +package com.introproventures.graphql.jpa.query.schema; + +import static com.introproventures.graphql.jpa.query.schema.model.book.Genre.NOVEL; +import static com.introproventures.graphql.jpa.query.schema.model.book.Genre.PLAY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.api.BDDAssertions.then; + +import java.io.IOException; +import java.util.Map; + +import javax.persistence.EntityManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + +import graphql.ExecutionResult; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@TestPropertySource({ "classpath:hibernate.properties" }) +public class GraphQLWhereVariableBindingsTests { + + @SpringBootApplication + static class Application { + + @Bean + public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaBuilder) { + return new GraphQLJpaExecutor(graphQLSchemaBuilder.build()); + } + + @Bean + public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { + return new GraphQLJpaSchemaBuilder(entityManager) + .name("GraphQLBooks") + .description("Books JPA test schema"); + } + + } + + @Autowired + private GraphQLExecutor executor; + + @Test + public void queryWithSimpleEqualsVariableBinding() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"title\": {" + + " \"EQ\": \"War and Peace\"" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .hasSize(1) + .extracting("id", "title", "genre") + .containsOnly(tuple(2L, "War and Peace", NOVEL)); + } + + @Test + public void queryWithNestedWhereClauseVariableBinding() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " author {" + + " id" + + " name" + + " }" + + " title" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"author\": {" + + " \"name\": {" + + " \"EQ\": \"Leo Tolstoy\"" + + " }" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .hasSize(2) + .extracting("title") + .containsOnly("War and Peace", "Anna Karenina"); + then(result) + .extracting("Books") + .flatExtracting("select") + .hasSize(2) + .extracting("author") + .extracting("id", "name") + .containsOnly(tuple(1L, "Leo Tolstoy")); + } + + @Test + public void queryWithInVariableBinding() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"genre\": {" + + " \"IN\": [\"PLAY\"]" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .extracting("genre") + .containsOnly(PLAY); + } + + @Test + public void queryWithMultipleRestrictionForOneProperty() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"id\": {" + + " \"GE\": 5," + + " \"LE\": 7" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .extracting("id", "title") + .containsOnly( + tuple(5L, "The Cherry Orchard"), + tuple(6L, "The Seagull"), + tuple(7L, "Three Sisters") + ); + } + + @Test + public void queryWithPropertyWhereVariableBinding() throws IOException { + //given + String query = "" + + "query($booksWhereClause: BooksCriteriaExpression) {" + + " Authors {" + + " select {" + + " name" + + " books(where: $booksWhereClause) {" + + " genre" + + " }" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"booksWhereClause\": {" + + " \"genre\": {" + + " \"IN\": [\"NOVEL\"]" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Authors") + .flatExtracting("select") + .extracting("name") + .containsOnly("Leo Tolstoy"); + then(result) + .extracting("Authors") + .flatExtracting("select") + .flatExtracting("books") + .extracting("genre") + .containsOnly(NOVEL); + } + + @Test + public void queryWithRestrictionsForMultipleProperties() throws IOException { + //given + String query = "" + + "query($where: BooksCriteriaExpression) {" + + " Books(where: $where) {" + + " select {" + + " id" + + " title" + + " }" + + " }" + + "}"; + String variables = "" + + "{" + + " \"where\": {" + + " \"title\": {" + + " \"LIKE\": \"The\"" + + " }," + + " \"id\": {" + + " \"LT\": 6" + + " }" + + " }" + + "}"; + + //when + ExecutionResult executionResult = executor.execute(query, getVariablesMap(variables)); + + // then + then(executionResult.getErrors()).isEmpty(); + Map result = executionResult.getData(); + then(result) + .extracting("Books") + .flatExtracting("select") + .extracting("id", "title") + .containsOnly(tuple(5L, "The Cherry Orchard")); + } + + @Test + public void queryBooksWithWhereVariableCriteriaEnumListExpression() throws IOException { + //given + String query = "query($where: BooksCriteriaExpression) {" + + " Books (where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + + String variables = "{" + + " \"where\": {" + + " \"genre\": {" + + " \"IN\": [\"NOVEL\"]" + + " }" + + " } " + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, getVariablesMap(variables)).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksWithWhereVariableCriteriaEnumExpression() throws IOException { + //given + String query = "query($where: BooksCriteriaExpression) {" + + " Books (where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + + String variables = "{" + + " \"where\": {" + + " \"genre\": {" + + " \"IN\": \"NOVEL\"" + + " }" + + " } " + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, getVariablesMap(variables)).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryBooksWithWhereVariableCriteriaEQEnumExpression() throws IOException { + //given + String query = "query($where: BooksCriteriaExpression) {" + + " Books (where: $where) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + "}"; + + String variables = "{" + + " \"where\": {" + + " \"genre\": {" + + " \"EQ\": \"NOVEL\"" + + " }" + + " } " + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, getVariablesMap(variables)).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @SuppressWarnings("unchecked") + private Map getVariablesMap(String variables) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + return (Map) mapper.readValue(variables, Map.class); + } +} \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java index bd0fc48c1..35f553c06 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/JavaScalarsTest.java @@ -19,11 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Month; -import java.time.ZoneId; +import java.time.*; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -210,5 +206,74 @@ public void testRegisterJavaScalarWithObjectCoercing() { assertThat(coercing).isInstanceOf(GraphQLObjectCoercing.class); assertThat(scalarType.getName()).isEqualTo("Map"); - } + } + @Test + public void string2OffsetDateTime() { + //given + Coercing coercing = JavaScalars.of(OffsetDateTime.class).getCoercing(); + final String input = "2017-02-02T12:30:15+07:00"; + + //when + Object result = coercing.serialize(input); + + //then + assertThat(result).isInstanceOf(OffsetDateTime.class); + + OffsetDateTime resultLDT = (OffsetDateTime) result; + + assert resultLDT.getDayOfMonth() == 2; + assert resultLDT.getMonth() == Month.FEBRUARY; + assert resultLDT.getYear() == 2017; + assert resultLDT.getHour() == 12; + assert resultLDT.getMinute() == 30; + assert resultLDT.getSecond() == 15; + assert resultLDT.getOffset() == ZoneOffset.of("+07:00"); + } + + @Test + public void string2ZonedDateTime() { + //given + Coercing coercing = JavaScalars.of(ZonedDateTime.class).getCoercing(); + final String input = "2019-08-05T13:47:57.428260700+07:00[Asia/Bangkok]"; + + //when + Object result = coercing.serialize(input); + + //then + assertThat(result).isInstanceOf(ZonedDateTime.class); + + ZonedDateTime resultLDT = (ZonedDateTime) result; + + assert resultLDT.getDayOfMonth() == 05; + assert resultLDT.getMonth() == Month.AUGUST; + assert resultLDT.getYear() == 2019; + assert resultLDT.getHour() == 13; + assert resultLDT.getMinute() == 47; + assert resultLDT.getSecond() == 57; + assert resultLDT.getNano() == 428260700; + assert resultLDT.getOffset() == ZoneOffset.of("+07:00"); + } + + @Test + public void string2Instant() { + //given + Coercing coercing = JavaScalars.of(Instant.class).getCoercing(); + final String input = "2019-08-05T07:15:07.199582Z"; + Instant instant = Instant.parse(input); + +// when + Object result = coercing.serialize(instant); + + //then + assertThat(result).isInstanceOf(Instant.class); + + OffsetDateTime resultLDT = OffsetDateTime.ofInstant((Instant) result, ZoneOffset.UTC); + + assert resultLDT.getYear() == 2019; + assert resultLDT.getDayOfMonth() == 05; + assert resultLDT.getMonth() == Month.AUGUST; + assert resultLDT.getHour() == 07; + assert resultLDT.getMinute() == 15; + assert resultLDT.getSecond() == 07; + } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java deleted file mode 100644 index ef172d009..000000000 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.introproventures.graphql.jpa.query.schema.impl; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; - -import com.introproventures.graphql.jpa.query.schema.model.calculated.CalculatedEntity; - -public class IntrospectionUtilsTest { - - // given - Class entity = CalculatedEntity.class; - - - @Test(expected=RuntimeException.class) - public void testIsTransientNonExisting() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity, "notFound")).isFalse(); - } - - @Test - public void testIsTransientClass() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity, "class")).isFalse(); - } - - @Test - public void testIsTransientFunction() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity, "fieldFun")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "hideFieldFunction")).isFalse(); - } - - @Test - public void testIsTransientFields() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity, "fieldFun")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "fieldMem")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "hideField")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "logic")).isTrue(); - } - - @Test - public void testNotTransientFields() throws Exception { - // given - Class entity = CalculatedEntity.class; - - // then - assertThat(IntrospectionUtils.isTransient(entity, "id")).isFalse(); - assertThat(IntrospectionUtils.isTransient(entity, "info")).isFalse(); - assertThat(IntrospectionUtils.isTransient(entity, "title")).isFalse(); - } - - @Test - public void testByPassSetMethod() throws Exception { - // given - Class entity = CalculatedEntity.class; - - // then - assertThat(IntrospectionUtils.isTransient(entity,"something")).isFalse(); - } -} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/book_superclass/BaseBook.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/book_superclass/BaseBook.java index 56eefae83..fae7f97f9 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/book_superclass/BaseBook.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/book_superclass/BaseBook.java @@ -20,4 +20,6 @@ public class BaseBook { SuperGenre genre; Date publicationDate; + + String[] tags; } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java index 8e26134f9..b08e3dd89 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/CalculatedEntity.java @@ -1,5 +1,8 @@ package com.introproventures.graphql.jpa.query.schema.model.calculated; +import java.time.LocalDate; +import java.time.Period; + import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Transient; @@ -8,19 +11,78 @@ import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + 2.1.1 Persistent Fields and Properties + + The persistent state of an entity is accessed by the persistence provider + runtime either via JavaBeans style property accessors or via instance variables. + A single access type (field or property access) applies to an entity hierarchy. + + When annotations are used, the placement of the mapping annotations on either + the persistent fields or persistent properties of the entity class specifies the + access type as being either field - or property - based access respectively. + + If the entity has field-based access, the persistence provider runtime accesses + instance variables directly. All non-transient instance variables that are not + annotated with the Transient annotation are persistent. When field-based access + is used, the object/relational mapping annotations for the entity class annotate + the instance variables. + + If the entity has property-based access, the persistence provider runtime accesses + persistent state via the property accessor methods. All properties not annotated with + the Transient annotation are persistent. The property accessor methods must be public + or protected. When property-based access is used, the object/relational mapping + annotations for the entity class annotate the getter property accessors. + + Mapping annotations cannot be applied to fields or properties that are transient or Transient. + + The behavior is unspecified if mapping annotations are applied to both persistent fields and + properties or if the XML descriptor specifies use of different access types within a class hierarchy. + */ @Data +@EqualsAndHashCode(callSuper = true) @Entity -public class CalculatedEntity { +public class CalculatedEntity extends ParentCalculatedEntity { @Id Long id; + @GraphQLDescription("title") String title; String info; + + @GraphQLDescription("Uppercase") + String Uppercase; + + private Integer age; + + private Integer getAge(){ + return Period.between(LocalDate.now(), + LocalDate.of(2000, 1, 1)) + .getYears(); + } + + private Integer protectedGetter; + + @GraphQLDescription("protectedGetter") + protected Integer getProtectedGetter(){ + return protectedGetter; + } + + String UppercaseGetter; + + @GraphQLDescription("transientModifier") + transient Integer transientModifier; // transient property + + @GraphQLIgnore + transient Integer transientModifierGraphQLIgnore; // transient property @Transient - boolean logic = true; + boolean logic = true; // transient property @Transient @GraphQLDescription("i desc member") @@ -30,6 +92,9 @@ public class CalculatedEntity { @GraphQLIgnore String hideField = "hideField"; + String propertyIgnoredOnGetter; + + String propertyDuplicatedInChild; @Transient @GraphQLDescription("i desc function") @@ -42,9 +107,34 @@ public boolean isCustomLogic() { return false; } + @GraphQLIgnore public String getHideFieldFunction() { return "getHideFieldFunction"; } public void setSomething(int a){} + + @GraphQLIgnore + public String getPropertyIgnoredOnGetter() { + return propertyIgnoredOnGetter; + } + + @Transient + @GraphQLIgnore + public String getIgnoredTransientValue(){ + return "IgnoredTransientValue"; + } + + @Transient + @GraphQLDescription("UppercaseGetter") + public String getUppercaseGetter() { + return Uppercase; + } + + // transient getter + @GraphQLIgnore + public String getUppercaseGetterIgnore() { + return UppercaseGetter; + } + } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/ParentCalculatedEntity.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/ParentCalculatedEntity.java new file mode 100644 index 000000000..ad3803344 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/calculated/ParentCalculatedEntity.java @@ -0,0 +1,58 @@ +package com.introproventures.graphql.jpa.query.schema.model.calculated; + +import javax.persistence.MappedSuperclass; +import javax.persistence.Transient; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; + +import lombok.Data; + +@GraphQLDescription("ParentCalculatedEntity description") +@MappedSuperclass +@Data +public class ParentCalculatedEntity { + + private Integer parentField; // persistent property + + @GraphQLDescription("parentTransientModifier") + private transient String parentTransientModifier; // transient property + + @GraphQLIgnore + private transient String parentTransientModifierGraphQLIgnore; // transient property + + @Transient + private String parentTransient; // transient property + + @GraphQLIgnore + @Transient + private String parentTransientGraphQLIgnore; // transient property + + @GraphQLIgnore + private String parentGraphQLIgnore; + + @Transient // transient getter property + private String parentTransientGetter; + + private String parentGraphQLIgnoreGetter; + + private String parentTransientGraphQLIgnoreGetter; + + private String propertyDuplicatedInChild; + + @GraphQLDescription("getParentTransientGetter") + public String getParentTransientGetter() { + return parentTransientGetter; + } + + @GraphQLIgnore + public String getParentTransientGraphQLIgnoreGetter() { + return parentTransientGraphQLIgnoreGetter; + } + + @GraphQLIgnore + public String getParentGraphQLIgnoreGetter() { + return parentGraphQLIgnoreGetter; + } + +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/metamodel/ClassWithCustomMetamodel.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/metamodel/ClassWithCustomMetamodel.java new file mode 100644 index 000000000..3a551c721 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/metamodel/ClassWithCustomMetamodel.java @@ -0,0 +1,52 @@ +package com.introproventures.graphql.jpa.query.schema.model.metamodel; + +import javax.persistence.Entity; +import javax.persistence.Id; + +import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore; + +@Entity +public class ClassWithCustomMetamodel { + + @Id + private Long id; + + private String publicValue; + + private String protectedValue; + + private String ignoredProtectedValue; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getPublicValue() { + return publicValue; + } + + public void setPublicValue(String publicValue) { + this.publicValue = publicValue; + } + + protected String getProtectedValue() { + return protectedValue; + } + + protected void setProtectedValue(String protectedValue) { + this.protectedValue = protectedValue; + } + + @GraphQLIgnore + protected String getIgnoredProtectedValue() { + return ignoredProtectedValue; + } + + protected void setIgnoredProtectedValue(String ignoredProtectedValue) { + this.ignoredProtectedValue = ignoredProtectedValue; + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/metamodel/ClassWithCustomMetamodel_.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/metamodel/ClassWithCustomMetamodel_.java new file mode 100644 index 000000000..e45ef03b8 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/metamodel/ClassWithCustomMetamodel_.java @@ -0,0 +1,18 @@ +package com.introproventures.graphql.jpa.query.schema.model.metamodel; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; + +@StaticMetamodel(ClassWithCustomMetamodel.class) +public class ClassWithCustomMetamodel_ { + + public static volatile SingularAttribute id; + public static volatile SingularAttribute publicValue; + public static volatile SingularAttribute protectedValue; + public static volatile SingularAttribute ignoredProtectedValue; + + public static final String ID = "id"; + public static final String PUBLIC_VALUE = "publicValue"; + public static final String PROTECTED_VALUE = "protectedValue"; + public static final String IGNORED_PROTECTED_VALUE = "ignoredProtectedValue"; +} diff --git a/graphql-jpa-query-schema/src/test/resources/EntityWithEmbeddedIdTest.sql b/graphql-jpa-query-schema/src/test/resources/EntityWithEmbeddedIdTest.sql new file mode 100644 index 000000000..555bab4f0 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/resources/EntityWithEmbeddedIdTest.sql @@ -0,0 +1,3 @@ +insert into Book(title, language, description) values +('War and Piece', 'Russian', 'War and Piece Novel'), +('Witch Of Water', 'English', 'Witch Of Water Fantasy'); \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/resources/EntityWithIdClassTest.sql b/graphql-jpa-query-schema/src/test/resources/EntityWithIdClassTest.sql new file mode 100644 index 000000000..69a324e98 --- /dev/null +++ b/graphql-jpa-query-schema/src/test/resources/EntityWithIdClassTest.sql @@ -0,0 +1,5 @@ +-- Json entity + +insert into ACCOUNT(account_number, account_type, description) values +('1', 'Savings', 'Saving account record'), +('2', 'Checking', 'Checking account record'); \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/resources/GraphQLJpaConverterTests.sql b/graphql-jpa-query-schema/src/test/resources/GraphQLJpaConverterTests.sql index 688c917fb..662e88ffd 100644 --- a/graphql-jpa-query-schema/src/test/resources/GraphQLJpaConverterTests.sql +++ b/graphql-jpa-query-schema/src/test/resources/GraphQLJpaConverterTests.sql @@ -20,4 +20,4 @@ insert into TASK_VARIABLE (create_time, execution_id, last_updated_time, name, p (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable4', 0, '2', 'json', '{"value":{"key":"data"}}'), (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable5', 1, '4', 'double', '{"value":1.2345}'), (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable6', 1, '4', 'int', '{"value":12345}'), - (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable7', 1, '4', 'json', '{"value":[1,2,3,4,5]}'); \ No newline at end of file + (CURRENT_TIMESTAMP, 'execution_id', CURRENT_TIMESTAMP, 'variable7', 1, '4', 'json', '{"value":[1,2,3,4,5]}'); \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/resources/LocalDatetTmeData.sql b/graphql-jpa-query-schema/src/test/resources/LocalDatetTmeData.sql new file mode 100644 index 000000000..47ce69c1b --- /dev/null +++ b/graphql-jpa-query-schema/src/test/resources/LocalDatetTmeData.sql @@ -0,0 +1,9 @@ +insert into LOCAL_DATE(id, LOCALDATETIME, OFFSETDATETIME, ZONEDDATETIME, INSTANT, LOCALDATE, description) values +('1', '2019-08-01 10:58:08.389991200', '2019-08-01 10:58:07.915990500+07:00','2019-08-01 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-01 03:58:08.842270400Z', '2019-08-01', 'Add test for LocalDate.'), +('2', '2019-08-02 10:58:08.389991200', '2019-08-02 10:58:07.915990500+07:00','2019-08-02 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-02 03:58:08.842270400Z', '2019-08-02', 'Add test for LocalDate.'), +('3', '2019-08-03 10:58:08.389991200', '2019-08-03 10:58:07.915990500+07:00','2019-08-03 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-03 03:58:08.842270400Z', '2019-08-03', 'Add test for LocalDate.'), +('4', '2019-08-04 10:58:08.389991200', '2019-08-04 10:58:07.915990500+07:00','2019-08-04 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-04 03:58:08.842270400Z', '2019-08-04', 'Add test for LocalDate.'), +('5', '2019-08-05 10:58:08.389991200', '2019-08-05 10:58:07.915990500+07:00','2019-08-05 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-05 03:58:08.842270400Z', '2019-08-05', 'Add test for LocalDate.'), +('6', '2019-08-06 10:58:08.389991200', '2019-08-06 10:58:07.915990500+07:00','2019-08-06 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-06 03:58:08.842270400Z', '2019-08-06', 'Add test for LocalDate.'), +('7', '2019-08-07 10:58:08.389991200', '2019-08-07 10:58:07.915990500+07:00','2019-08-07 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-07 03:58:08.842270400Z', '2019-08-07', 'Add test for LocalDate.'), +('8', '2019-08-08 10:58:08.389991200', '2019-08-08 10:58:07.915990500+07:00','2019-08-08 10:58:08.153991700+07:00[Asia/Bangkok]', '2019-08-08 03:58:08.842270400Z', '2019-08-08', 'Add test for LocalDate.'); \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/resources/data.sql b/graphql-jpa-query-schema/src/test/resources/data.sql index 53820a03d..4d32f5b5c 100644 --- a/graphql-jpa-query-schema/src/test/resources/data.sql +++ b/graphql-jpa-query-schema/src/test/resources/data.sql @@ -126,6 +126,12 @@ insert into book (id, title, author_id, genre, publication_date, description) values (7, 'Three Sisters', 4, 'PLAY', '1900-01-01', 'The play is sometimes included on the short list of Chekhov''s outstanding plays, along with The Cherry Orchard, The Seagull and Uncle Vanya.[1]'); insert into author (id, name, genre) values (8, 'Igor Dianov', 'JAVA'); +insert into book_tags (book_id, tags) values (2, 'war'), (2, 'piece'); +insert into book_tags (book_id, tags) values (3, 'anna'), (3, 'karenina'); +insert into book_tags (book_id, tags) values (5, 'cherry'), (5, 'orchard'); +insert into book_tags (book_id, tags) values (6, 'seagull'); +insert into book_tags (book_id, tags) values (7, 'three'), (7, 'sisters'); + insert into author_phone_numbers(phone_number, author_id) values ('1-123-1234', 1), ('1-123-5678', 1), diff --git a/graphql-jpa-query-web/pom.xml b/graphql-jpa-query-web/pom.xml index 170b5e469..37583511e 100644 --- a/graphql-jpa-query-web/pom.xml +++ b/graphql-jpa-query-web/pom.xml @@ -3,7 +3,7 @@ com.introproventures graphql-jpa-query-build - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT ../graphql-jpa-query-build graphql-jpa-query-web diff --git a/graphql-jpa-query-web/src/main/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfiguration.java b/graphql-jpa-query-web/src/main/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfiguration.java index 2423cd294..ba35ad16c 100644 --- a/graphql-jpa-query-web/src/main/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfiguration.java +++ b/graphql-jpa-query-web/src/main/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfiguration.java @@ -1,15 +1,18 @@ package com.introproventures.graphql.jpa.query.web.autoconfigure; -import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; -import com.introproventures.graphql.jpa.query.web.GraphQLController; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.web.GraphQLController; + @Configuration @ConditionalOnWebApplication @ConditionalOnClass(GraphQLExecutor.class) +@ConditionalOnProperty(prefix = "spring.graphql.jpa.query", name = {"enabled", "web.enabled"}, havingValue="true", matchIfMissing=true) public class GraphQLControllerAutoConfiguration { @Import(GraphQLController.class) diff --git a/graphql-jpa-query-web/src/test/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfigurationPropertyDisabledTest.java b/graphql-jpa-query-web/src/test/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfigurationPropertyDisabledTest.java new file mode 100644 index 000000000..3f56ff99b --- /dev/null +++ b/graphql-jpa-query-web/src/test/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfigurationPropertyDisabledTest.java @@ -0,0 +1,38 @@ +package com.introproventures.graphql.jpa.query.web.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.web.GraphQLController; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT, + properties = "spring.graphql.jpa.query.enabled=false") +public class GraphQLControllerAutoConfigurationPropertyDisabledTest { + + @MockBean + private GraphQLExecutor graphQLExecutor; + + @Autowired(required=false) + private GraphQLController graphQLController; + + @SpringBootApplication + static class Application { + + } + + @Test + public void contextLoads() { + assertThat(graphQLController).isNull(); + } + +} diff --git a/graphql-jpa-query-web/src/test/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfigurationWebPropertyDisabledTest.java b/graphql-jpa-query-web/src/test/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfigurationWebPropertyDisabledTest.java new file mode 100644 index 000000000..bc248dacd --- /dev/null +++ b/graphql-jpa-query-web/src/test/java/com/introproventures/graphql/jpa/query/web/autoconfigure/GraphQLControllerAutoConfigurationWebPropertyDisabledTest.java @@ -0,0 +1,39 @@ +package com.introproventures.graphql.jpa.query.web.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import com.introproventures.graphql.jpa.query.schema.GraphQLExecutor; +import com.introproventures.graphql.jpa.query.web.GraphQLController; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT, + properties = {"spring.graphql.jpa.query.enabled=true", + "spring.graphql.jpa.query.web.enabled=false"}) +public class GraphQLControllerAutoConfigurationWebPropertyDisabledTest { + + @MockBean + private GraphQLExecutor graphQLExecutor; + + @Autowired(required=false) + private GraphQLController graphQLController; + + @SpringBootApplication + static class Application { + + } + + @Test + public void contextLoads() { + assertThat(graphQLController).isNull(); + } + +} diff --git a/pom.xml b/pom.xml index c1ed8acb9..3adcbf567 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.introproventures graphql-jpa-query - 0.3.28-SNAPSHOT + 0.3.34-SNAPSHOT pom