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/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..6229997b4 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/EntityIntrospector.java @@ -0,0 +1,426 @@ +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 entity a Java entity class to introspect + * @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 entity a Java entity class to introspect + * @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 entity a Java entity class to introspect + * @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 entity a Java entity class to introspect + * @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. + */ + public static Stream iterate(T seed, Function> fetchNextFunction) { + Objects.requireNonNull(fetchNextFunction); + + 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 = fetchNextFunction.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/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 9f103766b..44733d32d 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 @@ -47,7 +47,7 @@ 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.EntityIntrospectionResult.AttributePropertyDescriptor; +import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; import graphql.Assert; @@ -651,26 +651,26 @@ private GraphQLObjectType computeObjectType(EntityType entityType) { .name(entityType.getName()) .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) - .filter(attribute -> !IntrospectionUtils.isIgnored(entityType.getJavaType(), attribute.getName())) - .map(it -> getObjectField(it, entityType)) - .collect(Collectors.toList()); - } - - private List getTransientFields(Class clazz) { - return IntrospectionUtils.introspect(clazz) - .getPropertyDescriptors().stream() - .filter(AttributePropertyDescriptor::isTransient) - .filter(AttributePropertyDescriptor::isNotIgnored) - .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" } ) @@ -678,8 +678,7 @@ private GraphQLFieldDefinition getJavaFieldDefinition(AttributePropertyDescripto GraphQLOutputType type = getGraphQLTypeFromJavaType(propertyDescriptor.getPropertyType()); DataFetcher dataFetcher = PropertyDataFetcher.fetching(propertyDescriptor.getName()); - String description = propertyDescriptor.getSchemaDescription() - .orElse(null); + String description = propertyDescriptor.getSchemaDescription().orElse(null); return GraphQLFieldDefinition.newFieldDefinition() .name(propertyDescriptor.getName()) @@ -876,21 +875,19 @@ protected final boolean isValidAssociation(Attribute attribute) { private String getSchemaDescription(Attribute attribute) { - return IntrospectionUtils.introspect(attribute.getDeclaringType() - .getJavaType()) - .getPropertyDescriptor(attribute) - .flatMap(AttributePropertyDescriptor::getSchemaDescription) + return EntityIntrospector.introspect(attribute.getDeclaringType()) + .getSchemaDescription(attribute.getName()) .orElse(null); } private String getSchemaDescription(EntityType entityType) { - return IntrospectionUtils.introspect(entityType.getJavaType()) + return EntityIntrospector.introspect(entityType) .getSchemaDescription() .orElse(null); } private String getSchemaDescription(EmbeddableType embeddableType) { - return IntrospectionUtils.introspect(embeddableType.getJavaType()) + return EntityIntrospector.introspect(embeddableType) .getSchemaDescription() .orElse(null); } 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 42b96af83..000000000 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtils.java +++ /dev/null @@ -1,341 +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.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -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.Transient; -import javax.persistence.metamodel.Attribute; - -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.schema.impl.IntrospectionUtils.EntityIntrospectionResult.AttributePropertyDescriptor; - -public class IntrospectionUtils { - private static final Logger log = LoggerFactory.getLogger(IntrospectionUtils.class); - - private static final Map, EntityIntrospectionResult> map = new LinkedHashMap<>(); - - public static EntityIntrospectionResult introspect(Class entity) { - return map.computeIfAbsent(entity, EntityIntrospectionResult::new); - } - - /** - * Test if Java bean property is transient according to JPA specification - * - * @param entity a Java entity class to introspect - * @param propertyName the name of the property - * @return true if property has Transient annotation or transient field modifier - * @throws RuntimeException if property does not exists - */ - public static boolean isTransient(Class entity, String propertyName) { - return introspect(entity).getPropertyDescriptor(propertyName) - .map(AttributePropertyDescriptor::isTransient) - .orElseThrow(() -> new RuntimeException(new NoSuchFieldException(propertyName))); - } - - /** - * Test if Java bean property is persistent according to JPA specification - * - * @param entity a Java entity class to introspect - * @param propertyName the name of the property - * @return true if property is persitent - * @throws RuntimeException if property does not exists - */ - public static boolean isPesistent(Class entity, String propertyName) { - return !isTransient(entity, propertyName); - } - - /** - * Test if entity property is annotated with GraphQLIgnore - * - * @param entity a Java entity class to introspect - * @param propertyName the name of the property - * @return true if property has GraphQLIgnore annotation - * @throws RuntimeException if property does not exists - */ - public static boolean isIgnored(Class entity, String propertyName) { - return introspect(entity).getPropertyDescriptor(propertyName) - .map(AttributePropertyDescriptor::isIgnored) - .orElseThrow(() -> new RuntimeException(new NoSuchFieldException(propertyName))); - } - - /** - * Test if entity property is not ignored - * - * @param entity a Java entity class to introspect - * @param propertyName the name of the property - * @return true if property has no GraphQLIgnore annotation - * @throws RuntimeException if property does not exists - */ - public static boolean isNotIgnored(Class entity, String propertyName) { - return !isIgnored(entity, propertyName); - } - - public static class EntityIntrospectionResult { - - private final Map descriptors; - private final Class entity; - private final BeanInfo beanInfo; - private final Map fields; - - @SuppressWarnings("rawtypes") - public EntityIntrospectionResult(Class entity) { - try { - this.beanInfo = Introspector.getBeanInfo(entity); - } catch (IntrospectionException cause) { - throw new RuntimeException(cause); - } - - this.entity = entity; - this.descriptors = Stream.of(beanInfo.getPropertyDescriptors()) - .map(AttributePropertyDescriptor::new) - .collect(Collectors.toMap(AttributePropertyDescriptor::getName, it -> it)); - - this.fields = getClasses().flatMap(k -> Arrays.stream(k.getDeclaredFields())) - .filter(f -> descriptors.containsKey(f.getName())) - .collect(Collectors.toMap(Field::getName, it -> it)); - } - - public Collection getPropertyDescriptors() { - return descriptors.values(); - } - - public Collection getFields() { - return fields.values(); - } - - public Optional getPropertyDescriptor(String fieldName) { - return Optional.ofNullable(descriptors.getOrDefault(fieldName, null)); - } - - public Optional getPropertyDescriptor(Attribute attribute) { - return getPropertyDescriptor(attribute.getName()); - } - - public boolean hasPropertyDescriptor(String fieldName) { - return descriptors.containsKey(fieldName); - } - - public Optional getField(String fieldName) { - return Optional.ofNullable(fields.get(fieldName)); - } - - public Class getEntity() { - return entity; - } - - public BeanInfo getBeanInfo() { - return beanInfo; - } - - public Optional getSchemaDescription() { - return getClasses().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 Stream> getClasses() { - return iterate(entity, k -> Optional.ofNullable(k.getSuperclass())); - } - - public class AttributePropertyDescriptor { - - private final PropertyDescriptor delegate; - - public AttributePropertyDescriptor(PropertyDescriptor delegate) { - this.delegate = delegate; - } - - public PropertyDescriptor getDelegate() { - return delegate; - } - - public Class getPropertyType() { - return delegate.getPropertyType(); - } - - public String getName() { - return delegate.getName(); - } - - public Optional getField() { - return Optional.ofNullable(fields.get(getName())); - } - - public boolean hasField() { - return getField().isPresent(); - } - - public Optional getReadMethod() { - return Optional.ofNullable(delegate.getReadMethod()); - } - - public Optional getAnnotation(Class annotationClass) { - return getReadMethod().map(m -> m.getAnnotation(annotationClass)) - .map(Optional::of) // Java 8 support - .orElseGet(() -> getField().map(f -> f.getAnnotation(annotationClass))); - } - - public Optional getSchemaDescription() { - return getAnnotation(GraphQLDescription.class).map(GraphQLDescription::value); - } - - public boolean hasSchemaDescription() { - return getSchemaDescription().isPresent(); - } - - public boolean isTransient() { - return isAnnotationPresent(Transient.class) - || hasFieldModifier(Modifier::isTransient); - } - - public boolean isIgnored() { - return isAnnotationPresent(GraphQLIgnore.class); - } - - public boolean isNotIgnored() { - return !isIgnored(); - } - - public boolean hasReadMethod() { - return getReadMethod().isPresent(); - } - - public boolean hasFieldModifier(Function test) { - return getField().map(it -> test.apply(it.getModifiers())) - .orElse(false); - } - - public boolean isAnnotationPresent(Class annotation) { - return isAnnotationPresentOnField(annotation) || isAnnotationPresentOnReadMethod(annotation); - } - - private boolean isAnnotationPresentOnField(Class annotation) { - return getField().map(f -> f.isAnnotationPresent(annotation)) - .orElse(false); - } - - private boolean isAnnotationPresentOnReadMethod(Class annotation) { - return getReadMethod().map(m -> m.isAnnotationPresent(annotation)) - .orElse(false); - } - - @Override - public String toString() { - return "EntityPropertyDescriptor [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(beanInfo, 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(beanInfo, other.beanInfo) && Objects.equals(entity, other.entity); - } - - @Override - public String toString() { - return "EntityIntrospectionResult [beanInfo=" + beanInfo + "]"; - } - - } - - /** - * 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. - */ - public static Stream iterate(T seed, Function> fetchNextFunction) { - Objects.requireNonNull(fetchNextFunction); - - 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 = fetchNextFunction.apply(v); - - return v; - } - }; - - return StreamSupport.stream( - Spliterators.spliteratorUnknownSize( iterator, Spliterator.ORDERED | Spliterator.IMMUTABLE), - false - ); - } -} 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 44d476878..05790b16f 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 @@ -22,6 +22,7 @@ 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; @@ -158,7 +159,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<>(); @@ -168,9 +169,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; @@ -975,32 +976,35 @@ else if (value instanceof EnumValue) { return ((FloatValue) value).getValue(); } else if (value instanceof ObjectValue) { Class javaType = getJavaType(environment, argument); + Map values = environment.getArgument(argument.getName()); try { - Object beanValue = javaType.getConstructor() - .newInstance(); - - Map objectValue = environment.getArgument(argument.getName()); - - objectValue.entrySet() - .stream() - .forEach(e -> { - setPropertyValue(beanValue, - e.getKey(), - e.getValue()); - }); - - return beanValue; - - } catch (Exception e1) { - // TODO + return getJavaBeanValue(javaType, values); + } catch (Exception cause) { + throw new RuntimeException(cause); } - } 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()); @@ -1016,7 +1020,7 @@ private void setPropertyValue(Object javaBean, String propertyName, Object prope } } } catch (Exception ignored) { - // TODO + // ignore } } @@ -1161,7 +1165,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()); } }); @@ -1260,6 +1264,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/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/schema/CalculatedEntityTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/CalculatedEntityTests.java index 044d298ed..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,16 +1,14 @@ package com.introproventures.graphql.jpa.query.schema; -import java.util.Optional; - 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.Optional; + import javax.persistence.EntityManager; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLSchema; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +23,8 @@ import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; import graphql.ExecutionResult; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLSchema; import graphql.validation.ValidationErrorType; @RunWith(SpringRunner.class) @@ -84,6 +84,7 @@ public void testIgnoreFields() { " fieldMem" + " fieldFun" + " logic" + + " age" + " customLogic" + " hideField" + " hideFieldFunction" + @@ -100,6 +101,10 @@ public void testIgnoreFields() { " parentTransientGraphQLIgnore" + " parentTransientModifierGraphQLIgnore" + " parentTransientGraphQLIgnoreGetter" + + " Uppercase" + + " UppercaseGetter" + + " UppercaseGetterIgnore" + + " protectedGetter" + " } " + " } " + "}"; @@ -121,7 +126,9 @@ public void testIgnoreFields() { 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", "transientModifierGraphQLIgnore")), + tuple(ValidationErrorType.FieldUndefined, list("CalculatedEntities", "select", "UppercaseGetterIgnore")) ); } 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/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 4fcc65c8c..000000000 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/impl/IntrospectionUtilsTest.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.introproventures.graphql.jpa.query.schema.impl; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.util.Optional; - -import javax.persistence.metamodel.Attribute; - -import org.junit.Test; -import org.mockito.Mockito; - -import com.introproventures.graphql.jpa.query.schema.impl.IntrospectionUtils.EntityIntrospectionResult; -import com.introproventures.graphql.jpa.query.schema.impl.IntrospectionUtils.EntityIntrospectionResult.AttributePropertyDescriptor; -import com.introproventures.graphql.jpa.query.schema.model.calculated.CalculatedEntity; -import com.introproventures.graphql.jpa.query.schema.model.calculated.ParentCalculatedEntity; - -public class IntrospectionUtilsTest { - - // given - private final Class entity = CalculatedEntity.class; - - @Test(expected = RuntimeException.class) - public void testIsTransientNonExisting() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity, "notFound")).isFalse(); - } - - @Test(expected = RuntimeException.class) - public void testIsIgnoredNonExisting() throws Exception { - // then - assertThat(IntrospectionUtils.isIgnored(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 testIsPersistentFunction() throws Exception { - // then - assertThat(IntrospectionUtils.isPesistent(entity, "fieldFun")).isFalse(); - assertThat(IntrospectionUtils.isPesistent(entity, "hideFieldFunction")).isTrue(); - } - - @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(); - assertThat(IntrospectionUtils.isTransient(entity, "transientModifier")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "parentTransientModifier")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "parentTransient")).isTrue(); - assertThat(IntrospectionUtils.isTransient(entity, "parentTransientGetter")).isTrue(); - } - - @Test - public void testNotTransientFields() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity, "id")).isFalse(); - assertThat(IntrospectionUtils.isTransient(entity, "info")).isFalse(); - assertThat(IntrospectionUtils.isTransient(entity, "title")).isFalse(); - assertThat(IntrospectionUtils.isTransient(entity, "parentField")).isFalse(); - } - - @Test - public void testByPassSetMethod() throws Exception { - // then - assertThat(IntrospectionUtils.isTransient(entity,"something")).isFalse(); - } - - @Test - public void shouldIgnoreMethodsThatAreAnnotatedWithGraphQLIgnore() { - //then - assertThat(IntrospectionUtils.isIgnored(entity, "propertyIgnoredOnGetter")).isTrue(); - assertThat(IntrospectionUtils.isIgnored(entity, "ignoredTransientValue")).isTrue(); - assertThat(IntrospectionUtils.isIgnored(entity, "hideField")).isTrue(); - assertThat(IntrospectionUtils.isIgnored(entity, "parentGraphQLIgnore")).isTrue(); - - assertThat(IntrospectionUtils.isIgnored(entity, "transientModifier")).isFalse(); - assertThat(IntrospectionUtils.isIgnored(entity, "parentTransientModifier")).isFalse(); - assertThat(IntrospectionUtils.isIgnored(entity, "parentTransient")).isFalse(); - assertThat(IntrospectionUtils.isIgnored(entity, "parentTransientGetter")).isFalse(); - } - - @Test - public void shouldNotIgnoreMethodsThatAreNotAnnotatedWithGraphQLIgnore() { - //then - assertThat(IntrospectionUtils.isNotIgnored(entity, "propertyIgnoredOnGetter")).isFalse(); - assertThat(IntrospectionUtils.isNotIgnored(entity, "ignoredTransientValue")).isFalse(); - assertThat(IntrospectionUtils.isNotIgnored(entity, "hideField")).isFalse(); - assertThat(IntrospectionUtils.isNotIgnored(entity, "parentGraphQLIgnore")).isFalse(); - - assertThat(IntrospectionUtils.isNotIgnored(entity, "transientModifier")).isTrue(); - assertThat(IntrospectionUtils.isNotIgnored(entity, "parentTransientModifier")).isTrue(); - assertThat(IntrospectionUtils.isNotIgnored(entity, "parentTransient")).isTrue(); - assertThat(IntrospectionUtils.isNotIgnored(entity, "parentTransientGetter")).isTrue(); - } - - @SuppressWarnings("rawtypes") - @Test - public void shouldGetClassesInHierarchy() { - //when - Class[] result = IntrospectionUtils.introspect(entity) - .getClasses() - .toArray(Class[]::new); - - //then - assertThat(result).containsExactly(CalculatedEntity.class, - ParentCalculatedEntity.class, - Object.class); - } - - @Test - public void testGetPropertyDescriptorsSchemaDescription() throws Exception { - // when - EntityIntrospectionResult result = IntrospectionUtils.introspect(CalculatedEntity.class); - - // then - assertThat(result.getPropertyDescriptors()) - .extracting(AttributePropertyDescriptor::getSchemaDescription) - .filteredOn(Optional::isPresent) - .extracting(Optional::get) - .containsOnly("title", - "transientModifier", - "i desc member", - "i desc function", - "getParentTransientGetter", - "parentTransientModifier"); - } - - @Test - public void testGetPropertyDescriptorSchemaDescriptionByAttribute() throws Exception { - Attribute attribute = Mockito.mock(Attribute.class); - - when(attribute.getName()).thenReturn("title"); - - // when - Optional result = IntrospectionUtils.introspect(CalculatedEntity.class) - .getPropertyDescriptor(attribute); - // then - assertThat(result.isPresent()).isTrue(); - } - - @Test - public void testGetParentEntitySchemaDescription() throws Exception { - // when - EntityIntrospectionResult result = IntrospectionUtils.introspect(CalculatedEntity.class); - - // then - assertThat(result.getSchemaDescription()).contains("ParentCalculatedEntity description"); - assertThat(result.hasSchemaDescription()).isTrue(); - } - -} 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 250e2b901..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; @@ -52,6 +55,26 @@ public class CalculatedEntity extends ParentCalculatedEntity { 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 @@ -70,6 +93,8 @@ public class CalculatedEntity extends ParentCalculatedEntity { String hideField = "hideField"; String propertyIgnoredOnGetter; + + String propertyDuplicatedInChild; @Transient @GraphQLDescription("i desc function") @@ -82,6 +107,7 @@ public boolean isCustomLogic() { return false; } + @GraphQLIgnore public String getHideFieldFunction() { return "getHideFieldFunction"; } @@ -98,4 +124,17 @@ public String getPropertyIgnoredOnGetter() { 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 index 83527d7fb..ad3803344 100644 --- 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 @@ -31,20 +31,21 @@ public class ParentCalculatedEntity { @GraphQLIgnore private String parentGraphQLIgnore; + @Transient // transient getter property private String parentTransientGetter; private String parentGraphQLIgnoreGetter; - private String parentTransientGraphQLIgnoreGetter; - - @Transient // transient getter property + private String parentTransientGraphQLIgnoreGetter; + + private String propertyDuplicatedInChild; + @GraphQLDescription("getParentTransientGetter") public String getParentTransientGetter() { return parentTransientGetter; } @GraphQLIgnore - @Transient // transient getter property public String getParentTransientGraphQLIgnoreGetter() { return parentTransientGraphQLIgnoreGetter; } 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"; +}