diff --git a/client/api/pom.xml b/client/api/pom.xml index ee05ed279..bec8906ed 100644 --- a/client/api/pom.xml +++ b/client/api/pom.xml @@ -37,6 +37,11 @@ provided + + io.smallrye + smallrye-graphql-client-model + + org.junit.jupiter junit-jupiter diff --git a/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java b/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java index c6e17b883..a0782cb3b 100644 --- a/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java +++ b/client/api/src/main/java/io/smallrye/graphql/client/typesafe/api/TypesafeGraphQLClientBuilder.java @@ -6,6 +6,7 @@ import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import io.smallrye.graphql.client.model.ClientModels; import io.smallrye.graphql.client.websocket.WebsocketSubprotocol; /** @@ -81,6 +82,8 @@ default TypesafeGraphQLClientBuilder headers(Map headers) { TypesafeGraphQLClientBuilder subprotocols(WebsocketSubprotocol... subprotocols); + TypesafeGraphQLClientBuilder clientModel(ClientModels.ClientModel clientModel); + TypesafeGraphQLClientBuilder allowUnexpectedResponseFields(boolean value); /** diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java index 175f8b1c7..84bc5fdf9 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java @@ -18,6 +18,7 @@ import io.smallrye.graphql.client.impl.GraphQLClientConfiguration; import io.smallrye.graphql.client.impl.GraphQLClientsConfiguration; import io.smallrye.graphql.client.impl.typesafe.reflection.MethodInvocation; +import io.smallrye.graphql.client.model.ClientModels; import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; import io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder; import io.smallrye.graphql.client.vertx.VertxClientOptionsHelper; @@ -48,6 +49,7 @@ public class VertxTypesafeGraphQLClientBuilder implements TypesafeGraphQLClientB private HttpClient httpClient; private Integer websocketInitializationTimeout; private Boolean allowUnexpectedResponseFields; + private ClientModels.ClientModel clientModel; public VertxTypesafeGraphQLClientBuilder() { this.subprotocols = new ArrayList<>(); @@ -123,6 +125,12 @@ public VertxTypesafeGraphQLClientBuilder subprotocols(WebsocketSubprotocol... su return this; } + @Override + public VertxTypesafeGraphQLClientBuilder clientModel(ClientModels.ClientModel clientModel) { + this.clientModel = clientModel; + return this; + } + @Override public TypesafeGraphQLClientBuilder allowUnexpectedResponseFields(boolean value) { this.allowUnexpectedResponseFields = value; @@ -167,14 +175,15 @@ public T build(Class apiClass) { dynamicHeaders = new HashMap<>(); } - VertxTypesafeGraphQLClientProxy graphQlClient = new VertxTypesafeGraphQLClientProxy(apiClass, headers, + VertxTypesafeGraphQLClientProxy graphQLClient = new VertxTypesafeGraphQLClientProxy(apiClass, clientModel, headers, dynamicHeaders, initPayload, endpoint, websocketUrl, executeSingleOperationsOverWebsocket, httpClient, webClient, subprotocols, websocketInitializationTimeout, allowUnexpectedResponseFields); + return apiClass.cast(Proxy.newProxyInstance(getClassLoader(apiClass), new Class[] { apiClass }, - (proxy, method, args) -> invoke(graphQlClient, method, args))); + (proxy, method, args) -> invoke(graphQLClient, method, args))); } private void applyConfigFor(Class apiClass) { diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java index 1a789634a..d177129e8 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java @@ -44,6 +44,7 @@ import io.smallrye.graphql.client.impl.typesafe.reflection.FieldInfo; import io.smallrye.graphql.client.impl.typesafe.reflection.MethodInvocation; import io.smallrye.graphql.client.impl.typesafe.reflection.TypeInfo; +import io.smallrye.graphql.client.model.ClientModels; import io.smallrye.graphql.client.vertx.websocket.BuiltinWebsocketSubprotocolHandlers; import io.smallrye.graphql.client.vertx.websocket.WebSocketSubprotocolHandler; import io.smallrye.graphql.client.websocket.WebsocketSubprotocol; @@ -77,6 +78,7 @@ class VertxTypesafeGraphQLClientProxy { private final List subprotocols; private final Integer subscriptionInitializationTimeout; private final Class api; + private final ClientModels.ClientModel clientModel; private final boolean executeSingleOperationsOverWebsocket; private final boolean allowUnexpectedResponseFields; @@ -91,6 +93,7 @@ class VertxTypesafeGraphQLClientProxy { VertxTypesafeGraphQLClientProxy( Class api, + ClientModels.ClientModel clientModel, Map additionalHeaders, Map> dynamicHeaders, Map initPayload, @@ -103,6 +106,7 @@ class VertxTypesafeGraphQLClientProxy { Integer subscriptionInitializationTimeout, boolean allowUnexpectedResponseFields) { this.api = api; + this.clientModel = clientModel; this.additionalHeaders = additionalHeaders; this.dynamicHeaders = dynamicHeaders; this.initPayload = initPayload; @@ -296,7 +300,12 @@ private Uni webSocketHandler() { private JsonObject request(MethodInvocation method) { JsonObjectBuilder request = jsonObjectFactory.createObjectBuilder(); - String query = queryCache.computeIfAbsent(method.getKey(), key -> new QueryBuilder(method).build()); + String query; + if (clientModel == null) { + query = queryCache.computeIfAbsent(method.getKey(), key -> new QueryBuilder(method).build()); + } else { + query = clientModel.getOperationMap().get(method.getMethodIdentifier()); + } request.add("query", query); request.add("variables", variables(method)); request.add("operationName", method.getName()); diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java index 2baec2410..24e7bffa8 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java @@ -21,18 +21,8 @@ public QueryBuilder(MethodInvocation method) { } public String build() { - StringBuilder request = new StringBuilder(); - switch (method.getOperationType()) { - case QUERY: - request.append("query "); - break; - case MUTATION: - request.append("mutation "); - break; - case SUBSCRIPTION: - request.append("subscription "); - break; - } + StringBuilder request = new StringBuilder(method.getOperationTypeAsString()); + request.append(" "); request.append(method.getName()); if (method.hasValueParameters()) request.append(method.valueParameters().map(this::declare).collect(joining(", ", "(", ")"))); diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java index 0214b3529..a8fad9c5a 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java @@ -243,4 +243,19 @@ public boolean isDeclaredInCloseable() { return method.getDeclaringClass().equals(Closeable.class) || method.getDeclaringClass().equals(AutoCloseable.class); } + + public String getMethodIdentifier() { + return getOperationTypeAsString() + "_" + getName(); + } + + public String getOperationTypeAsString() { + switch (getOperationType()) { + case MUTATION: + return "mutation"; + case SUBSCRIPTION: + return "subscription "; + default: + return "query"; + } + } } diff --git a/client/model-builder/pom.xml b/client/model-builder/pom.xml new file mode 100644 index 000000000..9ff9561b8 --- /dev/null +++ b/client/model-builder/pom.xml @@ -0,0 +1,192 @@ + + + 4.0.0 + + io.smallrye + smallrye-graphql-client-parent + 2.6.2-SNAPSHOT + + + smallrye-graphql-client-model-builder + SmallRye: GraphQL Client :: model builder + + + ${project.groupId} + smallrye-graphql-client-model + + + + + io.smallrye + jandex + + + + org.jboss.logging + jboss-logging + provided + + + + + org.junit.jupiter + junit-jupiter + + + io.smallrye + smallrye-graphql-client-api + + + org.eclipse.microprofile.graphql + microprofile-graphql-api + + + io.smallrye.reactive + mutiny + + + io.smallrye + smallrye-graphql-client + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java new file mode 100644 index 000000000..01bf6873d --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java @@ -0,0 +1,664 @@ +package io.smallrye.graphql.client.model; + +import static java.util.Collections.emptyMap; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.Type; + +import io.smallrye.graphql.client.model.helper.Direction; + +/** + * All the annotations we care about for a certain context + *

+ * There are multiple static methods to create the annotations for the correct context + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public class Annotations { + + private final Map annotationsMap; + + public final Map parentAnnotations; + + /** + * Get used when creating operations. + * Operation only have methods (no properties) + * + * @param methodInfo the java method + * @return Annotations for this method and its return-type + */ + public static Annotations getAnnotationsForMethod(MethodInfo methodInfo) { + Map annotationMap = new HashMap<>(); + + for (AnnotationInstance annotationInstance : methodInfo.annotations()) { + DotName name = annotationInstance.name(); + Kind kind = annotationInstance.target().kind(); + if (kind.equals(Kind.METHOD)) { + annotationMap.put(name, annotationInstance); + } + } + + final Type type = methodInfo.returnType(); + if (Classes.isParameterized(type)) { + Type wrappedType = type.asParameterizedType().arguments().get(0); + for (final AnnotationInstance annotationInstance : wrappedType.annotations()) { + DotName name = annotationInstance.name(); + annotationMap.put(name, annotationInstance); + } + } + + Map parentAnnotations = getParentAnnotations(methodInfo.declaringClass()); + + return new Annotations(annotationMap, parentAnnotations); + } + + private static Map getParentAnnotations(FieldInfo fieldInfo, MethodInfo methodInfo) { + ClassInfo declaringClass = fieldInfo != null ? fieldInfo.declaringClass() : methodInfo.declaringClass(); + return getParentAnnotations(declaringClass); + } + + private static Map getParentAnnotations(ClassInfo classInfo) { + Map parentAnnotations = new HashMap<>(); + + for (AnnotationInstance classAnnotation : classInfo.declaredAnnotations()) { + parentAnnotations.putIfAbsent(classAnnotation.name(), classAnnotation); + } + + Map packageAnnotations = getPackageAnnotations(classInfo); + for (DotName dotName : packageAnnotations.keySet()) { + parentAnnotations.putIfAbsent(dotName, packageAnnotations.get(dotName)); + } + + return parentAnnotations; + } + + private static Map getPackageAnnotations(ClassInfo classInfo) { + Map packageAnnotations = new HashMap<>(); + + DotName packageName = packageInfo(classInfo); + if (packageName != null) { + ClassInfo packageInfo = ScanningContext.getIndex().getClassByName(packageName); + if (packageInfo != null) { + for (AnnotationInstance packageAnnotation : packageInfo.declaredAnnotations()) { + packageAnnotations.putIfAbsent(packageAnnotation.name(), packageAnnotation); + } + } + } + + return packageAnnotations; + } + + private static DotName packageInfo(ClassInfo classInfo) { + String className = classInfo.name().toString(); + int index = className.lastIndexOf('.'); + if (index == -1) { + return null; + } + return DotName.createSimple(className.substring(0, index) + ".package-info"); + } + + /** + * Get used when creating fields on interfaces. + * Interfaces only has methods, no properties + * + * @param methodInfo the java method + * @return Annotations for this method + */ + public static Annotations getAnnotationsForInterfaceField(MethodInfo methodInfo) { + return getAnnotationsForOutputField(null, methodInfo); + } + + /** + * Get used when creating fields on inputs and types. + * This is used for public fields + * + * @param direction the direction + * @param fieldInfo the java property + * @return annotations for this field + */ + public static Annotations getAnnotationsForPojo(Direction direction, FieldInfo fieldInfo) { + return getAnnotationsForPojo(direction, fieldInfo, null); + } + + /** + * Get used when creating fields on inputs and types. + * Both has properties and methods and this needs to combined the two + * + * @param direction the direction + * @param fieldInfo the java property + * @param methodInfo the java method + * @return annotations for this field + */ + public static Annotations getAnnotationsForPojo(Direction direction, FieldInfo fieldInfo, MethodInfo methodInfo) { + if (direction.equals(Direction.IN)) { + return getAnnotationsForInputField(fieldInfo, methodInfo); + } else { + return getAnnotationsForOutputField(fieldInfo, methodInfo); + } + } + + public static Annotations getAnnotationsForInputCreator(MethodInfo method, short position, FieldInfo fieldInfo) { + Map annotationsForField = getAnnotationsForField(fieldInfo, null); + + if (fieldInfo != null) { + annotationsForField.putAll(getTypeUseAnnotations(fieldInfo.type())); + } + annotationsForField.putAll(getAnnotationsForArgument(method, position).annotationsMap); + + Map parentAnnotations = getParentAnnotations(fieldInfo, method); + + return new Annotations(annotationsForField, parentAnnotations); + } + + /** + * Get used when we create types and references to them + *

+ * Class level annotation for type creation. + * + * @param classInfo the java class + * @return annotation for this class + */ + public static Annotations getAnnotationsForClass(ClassInfo classInfo) { + + Map annotationMap = new HashMap<>(); + + for (AnnotationInstance annotationInstance : classInfo.declaredAnnotations()) { + DotName name = annotationInstance.name(); + annotationMap.put(name, annotationInstance); + } + + Map packageAnnotations = getPackageAnnotations(classInfo); + for (DotName dotName : packageAnnotations.keySet()) { + annotationMap.putIfAbsent(dotName, packageAnnotations.get(dotName)); + } + + return new Annotations(annotationMap, packageAnnotations); + } + + /** + * Get used when creating arrays. + *

+ * This will contains the annotation on the collection field and method + * + * @param typeInCollection the field java type + * @param methodTypeInCollection the method java type + * @return the annotation for this array + */ + public static Annotations getAnnotationsForArray(Type typeInCollection, + Type methodTypeInCollection) { + Map annotationMap = getAnnotationsForType(typeInCollection); + annotationMap.putAll(getAnnotationsForType(methodTypeInCollection)); + return new Annotations(annotationMap); + } + + // + + /** + * Used when we are creating operation and arguments for these operations + * + * @param methodInfo the java method + * @param pos the argument position + * @return annotation for this argument + */ + public static Annotations getAnnotationsForArgument(MethodInfo methodInfo, short pos) { + if (pos >= methodInfo.parametersCount()) { + throw new IndexOutOfBoundsException( + "Parameter at position " + pos + " not found on method " + methodInfo.name()); + } + + final Type parameterType = methodInfo.parameterType(pos); + + Map annotationMap = getAnnotations(parameterType); + + for (AnnotationInstance anno : methodInfo.annotations()) { + if (anno.target().kind().equals(Kind.METHOD_PARAMETER)) { + MethodParameterInfo methodParameter = anno.target().asMethodParameter(); + short position = methodParameter.position(); + if (position == pos) { + annotationMap.put(anno.name(), anno); + } + } + } + + final Map parentAnnotations = getParentAnnotations(methodInfo.declaringClass()); + + return new Annotations(annotationMap, parentAnnotations); + } + + public static boolean isJsonBAnnotation(AnnotationInstance instance) { + return instance.name().toString().startsWith(JAKARTA_JSONB) || instance.name().toString().startsWith(JAVAX_JSONB); + } + + // ------- All static creators done, now the actual class -------- + + private Annotations(Map annotations) { + this(annotations, new HashMap<>()); + } + + /** + * Create the annotations, mapped by name + * + * @param annotations the annotation + */ + private Annotations(Map annotations, Map parentAnnotations) { + this.annotationsMap = annotations; + this.parentAnnotations = parentAnnotations; + } + + public Set getAnnotationNames() { + return annotationsMap.keySet(); + } + + public Annotations removeAnnotations(DotName... annotations) { + Map newAnnotationsMap = new HashMap<>(annotationsMap); + for (DotName annotation : annotations) { + newAnnotationsMap.remove(annotation); + } + return new Annotations(newAnnotationsMap, this.parentAnnotations); + } + + /** + * Get a specific annotation + * + * @param annotation the annotation you want + * @return the annotation value or null + */ + public AnnotationValue getAnnotationValue(DotName annotation) { + return this.annotationsMap.get(annotation).value(); + } + + /** + * Get a specific annotation + * + * @param annotation the annotation you want + * @return the annotation value or null + */ + public AnnotationValue getAnnotationValue(DotName annotation, String name) { + return this.annotationsMap.get(annotation).value(name); + } + + /** + * Check if there is an annotation and it has a valid value + * + * @param annotation the annotation we are checking + * @return true if valid value + */ + public boolean containsKeyAndValidValue(DotName annotation) { + return this.annotationsMap.containsKey(annotation) && this.annotationsMap.get(annotation).value() != null; + } + + /** + * Check if one of these annotations is present + * + * @param annotations the annotations to check + * @return true if it does + */ + public boolean containsOneOfTheseAnnotations(DotName... annotations) { + for (DotName name : annotations) { + if (this.annotationsMap.containsKey(name)) { + return true; + } + } + return false; + } + + public boolean containsOneOfTheseInheritableAnnotations(DotName... annotations) { + for (DotName name : annotations) { + if (this.parentAnnotations.containsKey(name)) { + return true; + } + } + return false; + } + + /** + * Get on of these annotations + * + * @param annotations the annotations to check (in order) + * @return the annotation potentially or empty if not found + */ + public Optional getOneOfTheseAnnotations(DotName... annotations) { + for (DotName name : annotations) { + if (this.annotationsMap.containsKey(name)) { + return Optional.of(this.annotationsMap.get(name)); + } + } + return Optional.empty(); + } + + /** + * This go through a list of annotations and find the first one that has a valid value. + * If it could not find one, it return empty + * + * @param annotations the annotations in order + * @return the valid annotation value or default value + */ + public Optional getOneOfTheseAnnotationsValue(DotName... annotations) { + for (DotName dotName : annotations) { + if (dotName != null && containsKeyAndValidValue(dotName)) { + return getStringValue(dotName); + } + } + return Optional.empty(); + } + + /** + * This go through a list of method annotations and find the first one that has a valid value. + * If it could not find one, it return the default value. + * + * @param annotations the annotations in order + * @return the valid annotation value or empty + */ + public Optional getOneOfTheseMethodAnnotationsValue(DotName... annotations) { + for (DotName dotName : annotations) { + if (dotName != null && hasValidMethodAnnotation(dotName)) { + return getStringValue(dotName); + } + } + return Optional.empty(); + } + + /** + * This go through a list of method parameter annotations and find the first one that has a valid value. + * If it could not find one, it return the default value. + * + * @param annotations the annotations in order + * @return the valid annotation value or empty + */ + public Optional getOneOfTheseMethodParameterAnnotationsValue(DotName... annotations) { + for (DotName dotName : annotations) { + if (dotName != null && hasValidMethodParameterAnnotation(dotName)) { + return getStringValue(dotName); + } + } + return Optional.empty(); + } + + /** + * Get a stream of that annotation, maybe empty if not present, maybe a stream of one, or maybe several, if it's repeatable. + */ + public Stream resolve(DotName name) { + var annotationInstance = annotationsMap.get(name); + if (annotationInstance == null) { + var repeatableType = ScanningContext.getIndex().getClassByName(name); + if (repeatableType.hasAnnotation(REPEATABLE)) { + DotName containerName = repeatableType.annotation(REPEATABLE).value().asClass().name(); + AnnotationInstance containerAnnotation = annotationsMap.get(containerName); + if (containerAnnotation != null) { + return Stream.of(containerAnnotation.value().asNestedArray()); + } + } + return Stream.of(); + } + return Stream.of(annotationInstance); + } + + @Override + public String toString() { + return annotationsMap.toString(); + } + + private boolean hasValidMethodAnnotation(DotName annotation) { + return containsKeyAndValidValue(annotation) && isMethodAnnotation(getAnnotation(annotation)); + } + + private boolean hasValidMethodParameterAnnotation(DotName annotation) { + return containsKeyAndValidValue(annotation) && isMethodParameterAnnotation(getAnnotation(annotation)); + } + + private AnnotationInstance getAnnotation(DotName key) { + return this.annotationsMap.get(key); + } + + private Optional getStringValue(DotName annotation) { + AnnotationInstance annotationInstance = getAnnotation(annotation); + if (annotationInstance != null) { + return getStringValue(annotationInstance); + } + return Optional.empty(); + } + + private Optional getStringValue(AnnotationInstance annotationInstance) { + AnnotationValue value = annotationInstance.value(); + if (value != null) { + return getStringValue(value); + } + return Optional.empty(); + } + + private Optional getStringValue(AnnotationValue annotationValue) { + String value; + if (annotationValue != null) { + value = annotationValue.asString(); + if (value != null && !value.isEmpty()) { + return Optional.of(value); + } + } + return Optional.empty(); + } + + // Private static methods use by the static initializers + + private static boolean isMethodAnnotation(AnnotationInstance instance) { + return instance.target().kind().equals(Kind.METHOD); + } + + private static boolean isMethodParameterAnnotation(AnnotationInstance instance) { + return instance.target().kind().equals(Kind.METHOD_PARAMETER); + } + + private static Annotations getAnnotationsForInputField(FieldInfo fieldInfo, MethodInfo methodInfo) { + Map annotationsForField = getAnnotationsForField(fieldInfo, methodInfo); + + if (fieldInfo != null) { + annotationsForField.putAll(getTypeUseAnnotations(fieldInfo.type())); + } + if (methodInfo != null) { + List parameters = methodInfo.parameterTypes(); + if (!parameters.isEmpty()) { + Type param = parameters.get(ZERO); + annotationsForField.putAll(getTypeUseAnnotations(param)); + } + } + + final Map parentAnnotations = getParentAnnotations(fieldInfo, methodInfo); + + return new Annotations(annotationsForField, parentAnnotations); + } + + private static Annotations getAnnotationsForOutputField(FieldInfo fieldInfo, MethodInfo methodInfo) { + Map annotationsForField = getAnnotationsForField(fieldInfo, methodInfo); + + if (fieldInfo != null) { + annotationsForField.putAll(getTypeUseAnnotations(fieldInfo.type())); + } + if (methodInfo != null) { + Type returnType = methodInfo.returnType(); + if (returnType != null) { + annotationsForField.putAll(getTypeUseAnnotations(methodInfo.returnType())); + } + } + + Map parentAnnotations = getParentAnnotations(fieldInfo, methodInfo); + + return new Annotations(annotationsForField, parentAnnotations); + } + + private static Map getTypeUseAnnotations(Type type) { + if (type != null) { + return getAnnotationsWithFilter(type, + Annotations.DATE_FORMAT, + Annotations.NUMBER_FORMAT); + } + return emptyMap(); + } + + private static Map getAnnotations(Type type) { + Map annotationMap = new HashMap<>(); + + if (type.kind().equals(Type.Kind.PARAMETERIZED_TYPE)) { + Type typeInCollection = type.asParameterizedType().arguments().get(0); + annotationMap.putAll(getAnnotations(typeInCollection)); + } else { + List annotations = type.annotations(); + for (AnnotationInstance annotationInstance : annotations) { + annotationMap.put(annotationInstance.name(), annotationInstance); + } + } + + return annotationMap; + } + + private static Map getAnnotationsForType(Type type) { + Map annotationMap = new HashMap<>(); + for (AnnotationInstance annotationInstance : type.annotations()) { + DotName name = annotationInstance.name(); + annotationMap.put(name, annotationInstance); + } + return annotationMap; + } + + private static Map getAnnotationsForField(FieldInfo fieldInfo, MethodInfo methodInfo) { + Map annotationMap = new HashMap<>(); + if (fieldInfo != null) + annotationMap.putAll(listToMap(fieldInfo.annotations().stream() + .filter(ai -> ai.target().kind() == Kind.FIELD) + .collect(Collectors.toList()))); + if (methodInfo != null) + annotationMap.putAll(listToMap(methodInfo.annotations().stream() + .filter(ai -> ai.target().kind() == Kind.METHOD || ai.target().kind() == Kind.METHOD_PARAMETER) + .collect(Collectors.toList()))); + return annotationMap; + } + + private static Map listToMap(List annotationInstances) { + Map annotationMap = new HashMap<>(); + + for (AnnotationInstance annotationInstance : annotationInstances) { + DotName name = annotationInstance.name(); + annotationMap.put(name, annotationInstance); + } + return annotationMap; + } + + private static Map getAnnotationsWithFilter(Type type, DotName... filter) { + Map annotationMap = new HashMap<>(); + + if (type.kind().equals(Type.Kind.PARAMETERIZED_TYPE)) { + Type typeInCollection = type.asParameterizedType().arguments().get(0); + annotationMap.putAll(getAnnotationsWithFilter(typeInCollection, filter)); + } else { + List annotations = type.annotations(); + for (AnnotationInstance annotationInstance : annotations) { + if (Arrays.asList(filter).contains(annotationInstance.name())) { + annotationMap.put(annotationInstance.name(), annotationInstance); + } + } + } + + return annotationMap; + } + + private static final short ZERO = 0; + + public static final DotName REPEATABLE = DotName.createSimple("java.lang.annotation.Repeatable"); + + // SmallRye Common Annotations + public static final DotName BLOCKING = DotName.createSimple("io.smallrye.common.annotation.Blocking"); + public static final DotName NON_BLOCKING = DotName.createSimple("io.smallrye.common.annotation.NonBlocking"); + + // SmallRye GraphQL Annotations (Experimental) + public static final DotName TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.ToScalar"); // TODO: Remove + public static final DotName ADAPT_TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.AdaptToScalar"); + public static final DotName ADAPT_WITH = DotName.createSimple("io.smallrye.graphql.api.AdaptWith"); + public static final DotName ERROR_CODE = DotName.createSimple("io.smallrye.graphql.api.ErrorCode"); + public static final DotName DATAFETCHER = DotName.createSimple("io.smallrye.graphql.api.DataFetcher"); + public static final DotName SUBCRIPTION = DotName.createSimple("io.smallrye.graphql.api.Subscription"); + public static final DotName DIRECTIVE = DotName.createSimple("io.smallrye.graphql.api.Directive"); + public static final DotName DEFAULT_NON_NULL = DotName.createSimple("io.smallrye.graphql.api.DefaultNonNull"); + public static final DotName NULLABLE = DotName.createSimple("io.smallrye.graphql.api.Nullable"); + public static final DotName KOTLIN_METADATA = DotName.createSimple("kotlin.Metadata"); + + // MicroProfile GraphQL Annotations + public static final DotName GRAPHQL_CLIENT_API = DotName + .createSimple("io.smallrye.graphql.client.typesafe.api.GraphQLClientApi"); + public static final DotName QUERY = DotName.createSimple("org.eclipse.microprofile.graphql.Query"); + public static final DotName MUTATION = DotName.createSimple("org.eclipse.microprofile.graphql.Mutation"); + public static final DotName INPUT = DotName.createSimple("org.eclipse.microprofile.graphql.Input"); + public static final DotName TYPE = DotName.createSimple("org.eclipse.microprofile.graphql.Type"); + public static final DotName INTERFACE = DotName.createSimple("org.eclipse.microprofile.graphql.Interface"); + public static final DotName UNION = DotName.createSimple("io.smallrye.graphql.api.Union"); + + public static final DotName MULTIPLE = DotName.createSimple("io.smallrye.graphql.client.typesafe.api.Multiple"); + + public static final DotName NESTED_PARAMETER = DotName + .createSimple("io.smallrye.graphql.client.typesafe.api.NestedParameter"); + + public static final DotName ENUM = DotName.createSimple("org.eclipse.microprofile.graphql.Enum"); + public static final DotName ID = DotName.createSimple("org.eclipse.microprofile.graphql.Id"); + public static final DotName DESCRIPTION = DotName.createSimple("org.eclipse.microprofile.graphql.Description"); + public static final DotName DATE_FORMAT = DotName.createSimple("org.eclipse.microprofile.graphql.DateFormat"); + public static final DotName NUMBER_FORMAT = DotName.createSimple("org.eclipse.microprofile.graphql.NumberFormat"); + public static final DotName DEFAULT_VALUE = DotName.createSimple("org.eclipse.microprofile.graphql.DefaultValue"); + public static final DotName IGNORE = DotName.createSimple("org.eclipse.microprofile.graphql.Ignore"); + public static final DotName NON_NULL = DotName.createSimple("org.eclipse.microprofile.graphql.NonNull"); + public static final DotName NAME = DotName.createSimple("org.eclipse.microprofile.graphql.Name"); + public static final DotName SOURCE = DotName.createSimple("org.eclipse.microprofile.graphql.Source"); + + // Json-B Annotations + public static final String JAVAX_JSONB = "javax.json.bind.annotation."; + public static final DotName JAVAX_JSONB_DATE_FORMAT = DotName.createSimple(JAVAX_JSONB + "JsonbDateFormat"); + public static final DotName JAVAX_JSONB_NUMBER_FORMAT = DotName.createSimple(JAVAX_JSONB + "JsonbNumberFormat"); + public static final DotName JAVAX_JSONB_PROPERTY = DotName.createSimple(JAVAX_JSONB + "JsonbProperty"); + public static final DotName JAVAX_JSONB_TRANSIENT = DotName.createSimple(JAVAX_JSONB + "JsonbTransient"); + public static final DotName JAVAX_JSONB_CREATOR = DotName.createSimple(JAVAX_JSONB + "JsonbCreator"); + public static final DotName JAVAX_JSONB_TYPE_ADAPTER = DotName.createSimple(JAVAX_JSONB + "JsonbTypeAdapter"); + + public static final String JAKARTA_JSONB = "jakarta.json.bind.annotation."; + public static final DotName JAKARTA_JSONB_DATE_FORMAT = DotName.createSimple(JAKARTA_JSONB + "JsonbDateFormat"); + public static final DotName JAKARTA_JSONB_NUMBER_FORMAT = DotName.createSimple(JAKARTA_JSONB + "JsonbNumberFormat"); + public static final DotName JAKARTA_JSONB_PROPERTY = DotName.createSimple(JAKARTA_JSONB + "JsonbProperty"); + public static final DotName JAKARTA_JSONB_TRANSIENT = DotName.createSimple(JAKARTA_JSONB + "JsonbTransient"); + public static final DotName JAKARTA_JSONB_CREATOR = DotName.createSimple(JAKARTA_JSONB + "JsonbCreator"); + public static final DotName JAKARTA_JSONB_TYPE_ADAPTER = DotName.createSimple(JAKARTA_JSONB + "JsonbTypeAdapter"); + + // Jackson Annotations + public static final DotName JACKSON_IGNORE = DotName.createSimple("com.fasterxml.jackson.annotation.JsonIgnore"); + public static final DotName JACKSON_PROPERTY = DotName.createSimple("com.fasterxml.jackson.annotation.JsonProperty"); + public static final DotName JACKSON_CREATOR = DotName.createSimple("com.fasterxml.jackson.annotation.JsonCreator"); + public static final DotName JACKSON_FORMAT = DotName.createSimple("com.fasterxml.jackson.annotation.JsonFormat"); + + // Bean Validation Annotations (SmallRye extra, not part of the spec) + public static final DotName JAVAX_BEAN_VALIDATION_NOT_NULL = DotName.createSimple("javax.validation.constraints.NotNull"); + public static final DotName JAVAX_BEAN_VALIDATION_NOT_EMPTY = DotName.createSimple("javax.validation.constraints.NotEmpty"); + public static final DotName JAVAX_BEAN_VALIDATION_NOT_BLANK = DotName.createSimple("javax.validation.constraints.NotBlank"); + + public static final DotName JAKARTA_BEAN_VALIDATION_NOT_NULL = DotName + .createSimple("jakarta.validation.constraints.NotNull"); + public static final DotName JAKARTA_BEAN_VALIDATION_NOT_EMPTY = DotName + .createSimple("jakarta.validation.constraints.NotEmpty"); + public static final DotName JAKARTA_BEAN_VALIDATION_NOT_BLANK = DotName + .createSimple("jakarta.validation.constraints.NotBlank"); + + public static final DotName JAKARTA_NON_NULL = DotName.createSimple("jakarta.validation.constraints.NotNull"); + + //Kotlin NotNull + public static final DotName KOTLIN_NOT_NULL = DotName.createSimple("org.jetbrains.annotations.NotNull"); + +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Classes.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Classes.java new file mode 100644 index 000000000..f4c65bce9 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Classes.java @@ -0,0 +1,428 @@ +package io.smallrye.graphql.client.model; + +import static io.smallrye.graphql.client.model.ScanningContext.getIndex; + +import java.io.Serializable; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Deque; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.Stack; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.Vector; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + +import io.smallrye.graphql.client.typesafe.api.ErrorOr; +import io.smallrye.graphql.client.typesafe.api.TypesafeResponse; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +/** + * Class helper + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public class Classes { + + private Classes() { + } + + public static boolean isWrapper(Type type) { + return isParameterized(type) || isArray(type); + } + + /** + * Check if this is a Parameterized type + * + * @param type + * @return + */ + public static boolean isParameterized(Type type) { + return type.kind().equals(Type.Kind.PARAMETERIZED_TYPE); + } + + public static boolean isTypeVariable(Type type) { + return type.kind().equals(Type.Kind.TYPE_VARIABLE); + } + + /** + * Check if a certain type is Optional + * + * @param type the type + * @return true if it is + */ + public static boolean isOptional(Type type) { + return isParameterized(type) && type.name().equals(OPTIONAL) // Normal Optional + || type.name().equals(LONG_OPTIONAL) + || type.name().equals(DOUBLE_OPTIONAL) + || type.name().equals(INTEGER_OPTIONAL); + } + + public static boolean isOptionalInt(Type type) { + return type.name().equals(INTEGER_OPTIONAL); + } + + public static boolean isOptionalLong(Type type) { + return type.name().equals(LONG_OPTIONAL); + } + + public static boolean isOptionalDouble(Type type) { + return type.name().equals(DOUBLE_OPTIONAL); + } + + /** + * Check if a certain class is an interface + * + * @param classInfo the class to check + * @return true if it is + */ + public static boolean isInterface(ClassInfo classInfo) { + if (classInfo == null) + return false; + return Modifier.isInterface(classInfo.flags()); + } + + public static boolean isEnum(Type type) { + if (Classes.isClass(type)) { + ClassInfo clazz = getIndex().getClassByName(type.asClassType().name()); + return clazz != null && clazz.isEnum(); + } + return false; + } + + /** + * Check if this type is a Number (or collection of numbers) + * + * @param type the type to check + * @return true if it is + */ + public static boolean isNumberLikeTypeOrContainedIn(Type type) { + return isTypeOrContainedIn(type, BYTE, BYTE_PRIMATIVE, SHORT, SHORT_PRIMATIVE, INTEGER, INTEGER_PRIMATIVE, + BIG_INTEGER, DOUBLE, DOUBLE_PRIMATIVE, BIG_DECIMAL, LONG, LONG_PRIMATIVE, FLOAT, FLOAT_PRIMATIVE, + INTEGER_OPTIONAL, DOUBLE_OPTIONAL, LONG_OPTIONAL, INTEGER_ATOMIC, LONG_ATOMIC); + } + + /** + * Check if this type is a Date (or collection of numbers) + * + * @param type the type to check + * @return true if it is + */ + public static boolean isDateLikeTypeOrContainedIn(Type type) { + return isTypeOrContainedIn(type, LOCALDATE, LOCALTIME, LOCALDATETIME, ZONEDDATETIME, OFFSETDATETIME, OFFSETTIME, + UTIL_DATE, SQL_DATE, SQL_TIMESTAMP, SQL_TIME, INSTANT, CALENDAR, GREGORIAN_CALENDAR); + } + + private static boolean isTypeOrContainedIn(Type type, DotName... valid) { + switch (type.kind()) { + case PARAMETERIZED_TYPE: + // Container + Type typeInCollection = type.asParameterizedType().arguments().get(0); + return isTypeOrContainedIn(typeInCollection, valid); + case ARRAY: + // Array + Type typeInArray = type.asArrayType().component(); + return isTypeOrContainedIn(typeInArray, valid); + default: + for (DotName dotName : valid) { + if (type.name().toString().equals(dotName.toString())) { + return true; + } + } + return false; + } + } + + public static boolean isAsync(Type type) { + return isUni(type) + || isMulti(type); + } + + public static boolean isUni(Type type) { + return type.name().equals(UNI); + } + + public static boolean isMulti(Type type) { + return type.name().equals(MULTI); + } + + public static boolean isPrimitive(Type type) { + return type.kind().equals(Type.Kind.PRIMITIVE); + } + + public static boolean isVoid(Type type) { + return type.kind().equals(Type.Kind.VOID); + } + + public static boolean isClass(Type type) { + return type.kind().equals(Type.Kind.CLASS); + } + + /** + * Return true if type is java array, or it is Collection type which is handled as GraphQL array + * + * @param type to check + * @return if this is a collection or array + */ + public static boolean isCollectionOrArray(Type type) { + return type.kind().equals(Type.Kind.ARRAY) || isCollection(type); + } + + /** + * Return true if this is an array + * + * @param type + * @return + */ + public static boolean isArray(Type type) { + return type.kind().equals(Type.Kind.ARRAY); + } + + /** + * Return true if type is java Collection type which is handled as GraphQL array + * + * @param type to check + * @return if this is a collection + */ + public static boolean isCollection(Type type) { + if (isParameterized(type)) { + + ClassInfo clazz = ScanningContext.getIndex().getClassByName(type.name()); + if (clazz == null) { + // use classloader instead of jandex to handle basic java classes/interfaces + try { + Class clazzLoaded = Classes.class.getClassLoader().loadClass(type.name().toString()); + return Collection.class.isAssignableFrom(clazzLoaded); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Info not found in Jandex index nor classpath for class name:" + type.name()); + } + } + if (KNOWN_COLLECTIONS.contains(clazz.name())) { + return true; + } + + // we have to go recursively over all super-interfaces as Jandex provides only direct interfaces + // implemented in the class itself + for (Type intf : clazz.interfaceTypes()) { + if (isCollection(intf)) { + return true; + } + } + } + return false; + } + + public static boolean isMap(Type type) { + if (isParameterized(type)) { + + ClassInfo clazz = ScanningContext.getIndex().getClassByName(type.name()); + if (clazz == null) { + // use classloader instead of jandex to handle basic java classes/interfaces + try { + Class clazzLoaded = Classes.class.getClassLoader().loadClass(type.name().toString()); + return Map.class.isAssignableFrom(clazzLoaded); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Info not found in Jandex index nor classpath for class name:" + type.name()); + } + } + if (KNOWN_MAPS.contains(clazz.name())) { + return true; + } + + // we have to go recursively over all super-interfaces as Jandex provides only direct interfaces + // implemented in the class itself + for (Type intf : clazz.interfaceTypes()) { + if (isMap(intf)) { + return true; + } + } + } + return false; + } + + /** + * Return true if given type is parametrized type unwrapped/handled by the runtime before the serialization + * (Optional<>, CompletableFutur<>, CompletionStage<> etc) + * + * @param type to be checked + * @return true if type is unwrapped by the runtime + */ + @Deprecated + public static boolean isUnwrappedType(Type type) { + return isParameterized(type) && (isOptional(type) // Optional<> + || isAsync(type)) // CompletableFuture or CompletionStage + ; + } + + private static final DotName UNI = DotName.createSimple(Uni.class.getName()); + private static final DotName MULTI = DotName.createSimple(Multi.class.getName()); + + public static final DotName SERIALIZABLE = DotName.createSimple(Serializable.class.getName()); + public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); + + public static final DotName COLLECTION = DotName.createSimple(Collection.class.getName()); + public static final DotName LIST = DotName.createSimple(List.class.getName()); + public static final DotName LINKED_LIST = DotName.createSimple(LinkedList.class.getName()); + public static final DotName VECTOR = DotName.createSimple(Vector.class.getName()); + public static final DotName ARRAY_LIST = DotName.createSimple(ArrayList.class.getName()); + public static final DotName STACK = DotName.createSimple(Stack.class.getName()); + + public static final DotName SET = DotName.createSimple(Set.class.getName()); + public static final DotName HASH_SET = DotName.createSimple(HashSet.class.getName()); + public static final DotName SORTED_SET = DotName.createSimple(SortedSet.class.getName()); + public static final DotName TREE_SET = DotName.createSimple(TreeSet.class.getName()); + + public static final DotName QUEUE = DotName.createSimple(Queue.class.getName()); + public static final DotName DEQUE = DotName.createSimple(Deque.class.getName()); + + public static final DotName MAP = DotName.createSimple(Map.class.getName()); + public static final DotName HASH_MAP = DotName.createSimple(HashMap.class.getName()); + public static final DotName TREE_MAP = DotName.createSimple(TreeMap.class.getName()); + public static final DotName HASHTABLE = DotName.createSimple(Hashtable.class.getName()); + public static final DotName SORTED_MAP = DotName.createSimple(SortedMap.class.getName()); + + public static final DotName ENTRY = DotName.createSimple("io.smallrye.graphql.api.Entry"); + + public static final DotName OPTIONAL = DotName.createSimple(Optional.class.getName()); + + public static final DotName ENUM = DotName.createSimple(Enum.class.getName()); + + public static final DotName RECORD = DotName.createSimple("java.lang.Record"); + + public static final DotName TYPESAFE_RESPONSE = DotName.createSimple(TypesafeResponse.class.getName()); + public static final DotName ERROR_OR = DotName.createSimple(ErrorOr.class.getName()); + + public static final DotName LOCALDATE = DotName.createSimple(LocalDate.class.getName()); + public static final DotName LOCALDATETIME = DotName.createSimple(LocalDateTime.class.getName()); + public static final DotName LOCALTIME = DotName.createSimple(LocalTime.class.getName()); + public static final DotName ZONEDDATETIME = DotName.createSimple(ZonedDateTime.class.getName()); + public static final DotName OFFSETDATETIME = DotName.createSimple(OffsetDateTime.class.getName()); + public static final DotName OFFSETTIME = DotName.createSimple(OffsetTime.class.getName()); + public static final DotName INSTANT = DotName.createSimple(Instant.class.getName()); + + public static final DotName CALENDAR = DotName.createSimple(Calendar.class.getName()); + public static final DotName GREGORIAN_CALENDAR = DotName.createSimple(GregorianCalendar.class.getName()); + + public static final DotName PERIOD = DotName.createSimple(Period.class.getName()); + public static final DotName DURATION = DotName.createSimple(Duration.class.getName()); + + public static final DotName UTIL_DATE = DotName.createSimple(Date.class.getName()); + public static final DotName SQL_DATE = DotName.createSimple(java.sql.Date.class.getName()); + public static final DotName SQL_TIMESTAMP = DotName.createSimple(java.sql.Timestamp.class.getName()); + public static final DotName SQL_TIME = DotName.createSimple(java.sql.Time.class.getName()); + + private static final DotName BYTE = DotName.createSimple(Byte.class.getName()); + private static final DotName BYTE_PRIMATIVE = DotName.createSimple(byte.class.getName()); + + private static final DotName SHORT = DotName.createSimple(Short.class.getName()); + private static final DotName SHORT_PRIMATIVE = DotName.createSimple(short.class.getName()); + + private static final DotName INTEGER = DotName.createSimple(Integer.class.getName()); + private static final DotName INTEGER_PRIMATIVE = DotName.createSimple(int.class.getName()); + private static final DotName INTEGER_OPTIONAL = DotName.createSimple(OptionalInt.class.getName()); + private static final DotName INTEGER_ATOMIC = DotName.createSimple(AtomicInteger.class.getName()); + + private static final DotName BIG_INTEGER = DotName.createSimple(BigInteger.class.getName()); + + private static final DotName DOUBLE = DotName.createSimple(Double.class.getName()); + private static final DotName DOUBLE_PRIMATIVE = DotName.createSimple(double.class.getName()); + private static final DotName DOUBLE_OPTIONAL = DotName.createSimple(OptionalDouble.class.getName()); + + private static final DotName BIG_DECIMAL = DotName.createSimple(BigDecimal.class.getName()); + + private static final DotName LONG = DotName.createSimple(Long.class.getName()); + private static final DotName LONG_PRIMATIVE = DotName.createSimple(long.class.getName()); + private static final DotName LONG_OPTIONAL = DotName.createSimple(OptionalLong.class.getName()); + private static final DotName LONG_ATOMIC = DotName.createSimple(AtomicLong.class.getName()); + + private static final DotName FLOAT = DotName.createSimple(Float.class.getName()); + private static final DotName FLOAT_PRIMATIVE = DotName.createSimple(float.class.getName()); + + // Adapters + public static final DotName JAVAX_JSONB_ADAPTER = DotName.createSimple("javax.json.bind.adapter.JsonbAdapter"); + public static final DotName JAKARTA_JSONB_ADAPTER = DotName.createSimple("jakarta.json.bind.adapter.JsonbAdapter"); + public static final DotName ADAPTER = DotName.createSimple("io.smallrye.graphql.api.Adapter"); + + // Validation + public static final DotName JAVAX_VALIDATION_ANNOTATION_EMAIL = DotName.createSimple("javax.validation.constraints.Email"); + public static final DotName JAVAX_VALIDATION_ANNOTATION_MAX = DotName.createSimple("javax.validation.constraints.Max"); + public static final DotName JAVAX_VALIDATION_ANNOTATION_DECIMAL_MAX = DotName + .createSimple("javax.validation.constraints.DecimalMax"); + public static final DotName JAVAX_VALIDATION_ANNOTATION_MIN = DotName.createSimple("javax.validation.constraints.Min"); + public static final DotName JAVAX_VALIDATION_ANNOTATION_DECIMAL_MIN = DotName + .createSimple("javax.validation.constraints.DecimalMin"); + public static final DotName JAVAX_VALIDATION_ANNOTATION_PATTERN = DotName + .createSimple("javax.validation.constraints.Pattern"); + public static final DotName JAVAX_VALIDATION_ANNOTATION_SIZE = DotName.createSimple("javax.validation.constraints.Size"); + + public static final DotName JAKARTA_VALIDATION_ANNOTATION_EMAIL = DotName + .createSimple("jakarta.validation.constraints.Email"); + public static final DotName JAKARTA_VALIDATION_ANNOTATION_MAX = DotName.createSimple("jakarta.validation.constraints.Max"); + public static final DotName JAKARTA_VALIDATION_ANNOTATION_DECIMAL_MAX = DotName + .createSimple("jakarta.validation.constraints.DecimalMax"); + public static final DotName JAKARTA_VALIDATION_ANNOTATION_MIN = DotName.createSimple("jakarta.validation.constraints.Min"); + public static final DotName JAKARTA_VALIDATION_ANNOTATION_DECIMAL_MIN = DotName + .createSimple("jakarta.validation.constraints.DecimalMin"); + public static final DotName JAKARTA_VALIDATION_ANNOTATION_PATTERN = DotName + .createSimple("jakarta.validation.constraints.Pattern"); + public static final DotName JAKARTA_VALIDATION_ANNOTATION_SIZE = DotName + .createSimple("jakarta.validation.constraints.Size"); + + private static final List KNOWN_COLLECTIONS = new ArrayList<>(); + private static final List KNOWN_MAPS = new ArrayList<>(); + static { + KNOWN_COLLECTIONS.add(COLLECTION); + KNOWN_COLLECTIONS.add(LIST); + KNOWN_COLLECTIONS.add(LINKED_LIST); + KNOWN_COLLECTIONS.add(VECTOR); + KNOWN_COLLECTIONS.add(ARRAY_LIST); + KNOWN_COLLECTIONS.add(STACK); + KNOWN_COLLECTIONS.add(SET); + KNOWN_COLLECTIONS.add(HASH_SET); + KNOWN_COLLECTIONS.add(SORTED_SET); + KNOWN_COLLECTIONS.add(TREE_SET); + KNOWN_COLLECTIONS.add(QUEUE); + KNOWN_COLLECTIONS.add(DEQUE); + + KNOWN_MAPS.add(MAP); + KNOWN_MAPS.add(HASH_MAP); + KNOWN_MAPS.add(TREE_MAP); + KNOWN_MAPS.add(HASHTABLE); + KNOWN_MAPS.add(SORTED_MAP); + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java new file mode 100644 index 000000000..ca1e506c7 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java @@ -0,0 +1,85 @@ +package io.smallrye.graphql.client.model; + +import static io.smallrye.graphql.client.model.Annotations.GRAPHQL_CLIENT_API; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; + +import io.smallrye.graphql.client.model.helper.MethodModel; +import io.smallrye.graphql.client.model.helper.TypeAutoNameStrategy; + +public class ClientModelBuilder { + private static final Logger LOG = Logger.getLogger(ClientModelBuilder.class.getName()); + + /** + * This builds the Model from Jandex + * + * @param index the Jandex index + * @return the Schema + */ + public static ClientModels build(IndexView index) { + return build(index, TypeAutoNameStrategy.Default); + } + + /** + * This builds the Schema from Jandex + * + * @param index the Jandex index + * @param autoNameStrategy the naming strategy + * @return the Schema + */ + public static ClientModels build(IndexView index, TypeAutoNameStrategy autoNameStrategy) { + ScanningContext.register(index); + return new ClientModelBuilder(autoNameStrategy).generateClientModel(); + } + + private ClientModelBuilder(TypeAutoNameStrategy autoNameStrategy) { + } + + private ClientModels generateClientModel() { + ClientModels clientModel = new ClientModels(); + Map clientModelMap = new HashMap<>(); + + // Get all the @GraphQLClientApi annotations + Collection graphQLApiAnnotations = ScanningContext.getIndex() + .getAnnotations(GRAPHQL_CLIENT_API); + + graphQLApiAnnotations.forEach(graphQLApiAnnotation -> { + ClientModels.ClientModel operationMap = new ClientModels.ClientModel(); + ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); + List methods = getAllMethodsIncludingFromSuperClasses(apiClass); + methods.stream().forEach(method -> operationMap.getOperationMap() + .put(MethodModel.of(method).getMethodIdentifier(), new QueryBuilder(method).build())); + clientModelMap.put(graphQLApiAnnotation.value("configKey").asString(), + operationMap); + }); + clientModel.setClientModelMap(clientModelMap); + return clientModel; + } + + private List getAllMethodsIncludingFromSuperClasses(ClassInfo classInfo) { + ClassInfo current = classInfo; + IndexView index = ScanningContext.getIndex(); + List methods = new ArrayList<>(); + while (current != null) { + current.methods().stream().filter(methodInfo -> !methodInfo.isSynthetic()).forEach(methods::add); + DotName superName = classInfo.superName(); + if (superName != null) { + current = index.getClassByName(current.superName()); + } else { + current = null; + } + } + return methods; + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java new file mode 100644 index 000000000..4fc17fde7 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java @@ -0,0 +1,47 @@ +package io.smallrye.graphql.client.model; + +import static io.smallrye.graphql.client.model.helper.MethodModel.of; +import static java.util.stream.Collectors.joining; + +import org.jboss.jandex.MethodInfo; + +import io.smallrye.graphql.client.model.helper.DirectiveInstance; +import io.smallrye.graphql.client.model.helper.MethodModel; + +public class QueryBuilder { + private final MethodModel method; + + public QueryBuilder(MethodInfo method) { + this.method = of(method); + } + + public String build() { + StringBuilder request = new StringBuilder(method.getOperationTypeAsString()); + request.append(" "); + request.append(method.getName()); // operationName + if (method.hasValueParameters()) { + request.append(method.valueParameters().stream().map(method::declare).collect(joining(", ", "(", ")"))); + } + + if (method.isSingle()) { + request.append(" { "); + request.append(method.getName()); + if (method.hasRootParameters()) { + request.append(method.rootParameters().stream() + .map(method::bind) + .collect(joining(", ", "(", ")"))); + } + + if (method.hasDirectives()) { + request.append(method.getDirectives().stream().map(DirectiveInstance::buildDirective).collect(joining())); + } + } + + request.append(method.fields(method.getReturnType())); + + if (method.isSingle()) + request.append(" }"); + + return request.toString(); + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Scalars.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Scalars.java new file mode 100644 index 000000000..a7075190c --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Scalars.java @@ -0,0 +1,144 @@ +package io.smallrye.graphql.client.model; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Here we keep all the scalars we know about + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public class Scalars { + private static final Map scalarMap = new HashMap<>(); + private static final String STRING = "String"; + private static final String BOOLEAN = "Boolean"; + private static final String INTEGER = "Int"; + private static final String FLOAT = "Float"; + private static final String BIGINTEGER = "BigInteger"; + private static final String BIGDECIMAL = "BigDecimal"; + private static final String DATE = "Date"; + private static final String TIME = "Time"; + private static final String DATETIME = "DateTime"; + private static final String ID = "ID"; + private static final String PERIOD = "Period"; + private static final String DURATION = "Duration"; + private static final String VOID = "Void"; + + private Scalars() { + } + + public static boolean isScalar(String className) { + return scalarMap.containsKey(className); + } + + public static String getScalar(String identifier) { + return scalarMap.get(identifier); + } + + static { + // The main java type should go first. + + // Strings + populateScalar(String.class.getName(), STRING); + populateScalar(char.class.getName(), STRING); + populateScalar(Character.class.getName(), STRING); + populateScalar(UUID.class.getName(), STRING); + populateScalar(URL.class.getName(), STRING); + populateScalar(URI.class.getName(), STRING); + populateScalar("org.bson.types.ObjectId", STRING); + populateScalar("javax.json.JsonObject", STRING); + populateScalar("javax.json.JsonArray", STRING); + populateScalar("jakarta.json.JsonObject", STRING); + populateScalar("jakarta.json.JsonArray", STRING); + + // Boolean + populateScalar(Boolean.class.getName(), BOOLEAN); + populateScalar(boolean.class.getName(), BOOLEAN); + populateScalar(AtomicBoolean.class.getName(), BOOLEAN); + + // Integer + populateScalar(Integer.class.getName(), INTEGER); + populateScalar(int.class.getName(), INTEGER); + populateScalar(Short.class.getName(), INTEGER); + populateScalar(short.class.getName(), INTEGER); + populateScalar(Byte.class.getName(), INTEGER); + populateScalar(byte.class.getName(), INTEGER); + populateScalar(OptionalInt.class.getName(), INTEGER); + populateScalar(AtomicInteger.class.getName(), INTEGER); + + // Float + populateScalar(Float.class.getName(), FLOAT); + populateScalar(float.class.getName(), FLOAT); + populateScalar(Double.class.getName(), FLOAT); + populateScalar(double.class.getName(), FLOAT); + populateScalar(OptionalDouble.class.getName(), FLOAT); + + // BigInteger + populateScalar(BigInteger.class.getName(), BIGINTEGER); + populateScalar(Long.class.getName(), BIGINTEGER); + populateScalar(long.class.getName(), BIGINTEGER); + populateScalar(OptionalLong.class.getName(), BIGINTEGER); + populateScalar(AtomicLong.class.getName(), BIGINTEGER); + + // BigDecimal + populateScalar(BigDecimal.class.getName(), BIGDECIMAL); + + // Date + populateScalar(LocalDate.class.getName(), DATE); + populateScalar(Date.class.getName(), DATE); + + // Time + populateScalar(LocalTime.class.getName(), TIME); + populateScalar(Time.class.getName(), TIME); + populateScalar(OffsetTime.class.getName(), TIME); + + // DateTime + populateScalar(LocalDateTime.class.getName(), DATETIME); + populateScalar(java.util.Date.class.getName(), DATETIME); + populateScalar(Timestamp.class.getName(), DATETIME); + populateScalar(ZonedDateTime.class.getName(), DATETIME); + populateScalar(OffsetDateTime.class.getName(), DATETIME); + populateScalar(Instant.class.getName(), DATETIME); + populateScalar(Calendar.class.getName(), DATETIME); + populateScalar(GregorianCalendar.class.getName(), DATETIME); + + // Duration + populateScalar(Duration.class.getName(), DURATION); + // Period + populateScalar(Period.class.getName(), PERIOD); + + // Void + populateScalar(Void.class.getName(), VOID); + populateScalar(void.class.getName(), VOID); + } + + private static void populateScalar(String className, String scalarName) { + scalarMap.put(className, scalarName); + } + +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ScanningContext.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ScanningContext.java new file mode 100644 index 000000000..ce718bf0d --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ScanningContext.java @@ -0,0 +1,33 @@ +package io.smallrye.graphql.client.model; + +import org.jboss.jandex.IndexView; + +/** + * A simple registry to hold the current scanning info + * + * At this point we only keep the index in the context + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public class ScanningContext { + private static final ThreadLocal current = new ThreadLocal<>(); + + public static void register(IndexView index) { + ScanningContext registry = new ScanningContext(index); + current.set(registry); + } + + public static IndexView getIndex() { + return current.get().index; + } + + public static void remove() { + current.remove(); + } + + private final IndexView index; + + private ScanningContext(final IndexView index) { + this.index = index; + } +} \ No newline at end of file diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/Direction.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/Direction.java new file mode 100644 index 000000000..c2607b341 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/Direction.java @@ -0,0 +1,11 @@ +package io.smallrye.graphql.client.model.helper; + +/** + * Indicating the direction + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public enum Direction { + IN, + OUT +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveHelper.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveHelper.java new file mode 100644 index 000000000..427a44292 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveHelper.java @@ -0,0 +1,53 @@ +package io.smallrye.graphql.client.model.helper; + +import static io.smallrye.graphql.client.model.Annotations.DIRECTIVE; +import static io.smallrye.graphql.client.model.Annotations.REPEATABLE; +import static io.smallrye.graphql.client.model.ScanningContext.getIndex; +import static java.util.Arrays.stream; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; + +public class DirectiveHelper { + + public static Stream resolveDirectives(Stream annotationInstances, + String directiveLocation, + AnnotationTarget.Kind targetKind) { + return resolveDirectives(annotationInstances + .filter(annotation -> annotation.target().kind() == targetKind), directiveLocation); + } + + public static Stream resolveDirectives(Stream annotationInstances, + String directiveLocation) { + return annotationInstances + .flatMap(annotationInstance -> { + ClassInfo scannedAnnotation = getIndex().getClassByName(annotationInstance.name()); + if (scannedAnnotation != null) { + if (scannedAnnotation.hasAnnotation(DIRECTIVE)) { + return Stream.of(annotationInstance); + } + Optional repeatableAnnotation = getIndex() + .getAnnotations(REPEATABLE).stream() + .filter(annotation -> annotation.target().hasAnnotation(DIRECTIVE) + && annotation.value().asClass().name().equals(annotationInstance.name())) + .findFirst(); + if (repeatableAnnotation.isPresent()) { + return Stream.of(annotationInstance.value().asNestedArray()); + } + } + return Stream.empty(); + }) + .filter(annotation -> directiveFilter(annotation, directiveLocation)); + + } + + private static boolean directiveFilter(AnnotationInstance annotation, String directiveLocation) { + return stream( + getIndex().getClassByName(annotation.name()).annotation(DIRECTIVE).value("on").asEnumArray()) + .anyMatch(directiveLocation::equals); + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveInstance.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveInstance.java new file mode 100644 index 000000000..b6cea778f --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/DirectiveInstance.java @@ -0,0 +1,61 @@ +package io.smallrye.graphql.client.model.helper; + +import static java.util.stream.Collectors.toUnmodifiableMap; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; + +public class DirectiveInstance { + private final String name; + private final Map values; + + DirectiveInstance(AnnotationInstance annotationInstance) { + this.name = toDirectiveName(annotationInstance.name().withoutPackagePrefix()); + this.values = annotationInstance.values().stream().collect( + toUnmodifiableMap(AnnotationValue::name, AnnotationValue::value)); + } + + public static DirectiveInstance of(AnnotationInstance annotationInstance) { + return new DirectiveInstance(annotationInstance); + } + + public String buildDirective() { + StringBuilder builder = new StringBuilder(); + builder.append(" @") // space at the beginning for chaining multiple directives. + .append(name); + + if (!values.isEmpty()) { + builder + .append("(") + .append(buildDirectiveArgs()) + .append(")"); + } + + return builder.toString(); + } + + private String buildDirectiveArgs() { + return values.entrySet().stream() + .map(entry -> new StringBuilder() + .append(entry.getKey()) + .append(": ") + .append(entry.getValue() instanceof String ? "\"" + entry.getValue() + "\"" : entry.getValue()) + .toString()) + .collect(Collectors.joining(", ")); + + } + + private String toDirectiveName(String name) { + if (Character.isUpperCase(name.charAt(0))) + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + return name; + } + + @Override + public String toString() { + return "DirectiveInstance{" + "type=" + name + ", values=" + values + '}'; + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/FieldModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/FieldModel.java new file mode 100644 index 000000000..cdd587769 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/FieldModel.java @@ -0,0 +1,97 @@ +package io.smallrye.graphql.client.model.helper; + +import static io.smallrye.graphql.client.model.Annotations.NAME; +import static java.util.stream.Collectors.toList; + +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Optional; + +import org.eclipse.microprofile.graphql.Name; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; + +import io.smallrye.graphql.client.model.Classes; + +public class FieldModel implements NamedElement { + private FieldInfo field; + + private TypeModel rawType; // only if the Type is TypeVariable (in a custom created Class) + + private List directives; + + public static FieldModel of(FieldInfo field, TypeModel rawType) { + return new FieldModel(field, rawType); + } + + FieldModel(FieldInfo field, TypeModel rawType) { + this.field = field; + this.rawType = rawType; + this.directives = DirectiveHelper.resolveDirectives(field.annotations().stream(), getDirectiveLocation()) + .map(DirectiveInstance::of) + .collect(toList()); + } + + /** If the field is renamed with a {@link Name} annotation, the real field name is used as an alias. */ + public Optional getAlias() { + if (field.hasAnnotation(NAME)) { + return Optional.of(getRawName()); + } + return Optional.empty(); + } + + public String getName() { + if (field.hasAnnotation(NAME)) { + return field.annotation(NAME).value().asString(); + } + return getRawName(); + } + + public String getRawName() { + return field.name(); + } + + @Override + public String getDirectiveLocation() { + return "FIELD"; + } + + public boolean hasAnnotation(DotName annotation) { + return field.hasAnnotation(annotation); + } + + public boolean isStatic() { + return Modifier.isStatic(field.flags()); + } + + public boolean isTransient() { + return Modifier.isTransient(field.flags()); + } + + public boolean isSynthetic() { + return field.asField().isSynthetic(); + } + + public boolean isParameterized() { + return Classes.isParameterized(field.type()); + } + + public boolean isTypeVariable() { + return Classes.isTypeVariable(field.type()); + } + + public TypeModel getType() { + if (TypeModel.of(field.type()).isTypeVariable()) { + return rawType.getFirstRawType(); + } + return TypeModel.of(field.type()); + } + + public boolean hasDirectives() { + return !directives.isEmpty(); + } + + public List getDirectives() { + return directives; + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/MethodModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/MethodModel.java new file mode 100644 index 000000000..65715ffa1 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/MethodModel.java @@ -0,0 +1,239 @@ +package io.smallrye.graphql.client.model.helper; + +import static io.smallrye.graphql.client.model.Annotations.MULTIPLE; +import static io.smallrye.graphql.client.model.Annotations.MUTATION; +import static io.smallrye.graphql.client.model.Annotations.NAME; +import static io.smallrye.graphql.client.model.Annotations.QUERY; +import static io.smallrye.graphql.client.model.Annotations.SUBCRIPTION; +import static io.smallrye.graphql.client.model.ScanningContext.getIndex; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Optional; +import java.util.Stack; +import java.util.stream.Collectors; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.MethodInfo; + +import io.smallrye.graphql.client.core.OperationType; +import io.smallrye.graphql.client.impl.SmallRyeGraphQLClientMessages; + +public class MethodModel implements NamedElement { + private final MethodInfo method; + private final List parameters; + private final Stack typeStack = new Stack<>(); + private final Stack expressionStack = new Stack<>(); + private final List directives; + + public static MethodModel of(MethodInfo method) { + return new MethodModel(method); + } + + MethodModel(MethodInfo method) { + this.method = method; + this.parameters = method.parameters().stream().map(ParameterModel::of).collect(Collectors.toList()); + this.directives = DirectiveHelper.resolveDirectives(method.annotations().stream(), + getDirectiveLocation(), AnnotationTarget.Kind.METHOD) + .map(DirectiveInstance::of) + .collect(toList()); + } + + public String fields(TypeModel type) { + if (typeStack.contains(type.getName())) + throw SmallRyeGraphQLClientMessages.msg.fieldRecursionFound(); + try { + typeStack.push(type.getName()); + return recursionCheckedFields(type); + } finally { + typeStack.pop(); + } + } + + public String recursionCheckedFields(TypeModel type) { + while (type.isOptional() || type.isErrorOr() || type.isTypesafeResponse()) { + type = type.getFirstRawType(); // unwrapping + } + if (type.isScalar()) + return ""; + if (type.isCollectionOrArray() || type.isAsync()) + return fields(type.getItemTypeOrElementType()); + if (type.isMap()) { + String keyFields = fields(type.getMapKeyType()); + String valueFields = fields(type.getMapValueType()); + // FIXME: unnecessary whitespaces... + return " { key " + keyFields + " value " + valueFields + "}"; + } + return type.fields() + .map(this::field) + .collect(joining(" ", " { ", " }")); + } + + public String field(FieldModel field) { + TypeModel type = field.getType(); + StringBuilder expression = new StringBuilder(); + field.getAlias().ifPresent(alias -> expression.append(alias).append(": ")); + expression.append(field.getName()); + + if (field.hasDirectives()) { + expression.append(field.getDirectives().stream().map(DirectiveInstance::buildDirective).collect(joining())); + } + + String path = nestedExpressionPrefix() + field.getRawName(); + List nestedParameters = nestedParameters(path); + if (!nestedParameters.isEmpty()) { + expression.append(nestedParameters.stream() + .map(this::bind) + .collect(joining(", ", "(", ")"))); + } + + expressionStack.push(path); + expression.append(fields(type)); // appends the empty string, if the type is scalar, etc. + expressionStack.pop(); + + return expression.toString(); + } + + public String declare(ParameterModel parameter) { + return "$" + parameter.getRawName() + ": " + parameter.graphQlInputTypeName() + + ((parameter.hasDirectives()) ? parameter.getDirectiveInstances() + .stream() + .map(DirectiveInstance::buildDirective) + .collect(joining()) + : ""); + } + + public String bind(ParameterModel parameter) { + return parameter.getName() + ": $" + parameter.getRawName(); + } + + public String nestedExpressionPrefix() { + return expressionStack.isEmpty() ? "" : expressionStack.peek() + "."; + } + + public OperationType getOperationType() { + if (method.hasAnnotation(MUTATION)) { + return OperationType.MUTATION; + } + if (method.hasAnnotation(SUBCRIPTION)) { + return OperationType.SUBSCRIPTION; + } + return OperationType.QUERY; + } + + public String getName() { + return queryName() + .orElseGet(() -> mutationName() + .orElseGet(() -> subscriptionName() + .orElseGet(this::getRawName))); + } + + public Optional queryName() { + AnnotationInstance queryAnnotation = method.returnType().annotation(QUERY); + if (queryAnnotation != null && queryAnnotation.value() != null) + return Optional.of(queryAnnotation.value().asString()); + AnnotationInstance nameAnnotation = method.returnType().annotation(NAME); + if (nameAnnotation != null) + return Optional.of(nameAnnotation.value().asString()); + return Optional.empty(); + } + + public Optional mutationName() { + AnnotationInstance mutationAnnotation = method.returnType().annotation(MUTATION); + if (mutationAnnotation != null && mutationAnnotation.value() != null) + return Optional.of(mutationAnnotation.value().asString()); + AnnotationInstance nameAnnotation = method.returnType().annotation(NAME); + if (nameAnnotation != null) + return Optional.of(nameAnnotation.value().asString()); + return Optional.empty(); + } + + public Optional subscriptionName() { + AnnotationInstance subscriptionAnnotation = method.returnType().annotation(SUBCRIPTION); + if (subscriptionAnnotation != null && subscriptionAnnotation.value() != null) + return Optional.of(subscriptionAnnotation.value().asString()); + AnnotationInstance nameAnnotation = method.returnType().annotation(NAME); + if (nameAnnotation != null) + return Optional.of(nameAnnotation.value().asString()); + return Optional.empty(); + } + + public String getRawName() { + String name = method.name(); + if (name.startsWith("get") && name.length() > 3 && Character.isUpperCase(name.charAt(3))) + return Character.toLowerCase(name.charAt(3)) + name.substring(4); + return name; + } + + @Override + public String getDirectiveLocation() { + switch (getOperationType()) { + case MUTATION: + return "MUTATION"; + case SUBSCRIPTION: + return "SUBSCRIPTION"; + default: + return "QUERY"; + } + } + + public List valueParameters() { + return parameters.stream().filter(ParameterModel::isValueParameter).collect(Collectors.toList()); + } + + public List rootParameters() { + return parameters.stream().filter(ParameterModel::isRootParameter).collect(Collectors.toList()); + } + + public List nestedParameters(String path) { + return parameters.stream() + .filter(ParameterModel::isNestedParameter) + .filter(parameter -> parameter + .getNestedParameterNames() + .anyMatch(path::equals)) + .collect(toList()); + } + + public TypeModel getReturnType() { + return TypeModel.of(method.returnType()); + } + + public boolean hasValueParameters() { + return !valueParameters().isEmpty(); + } + + public boolean hasRootParameters() { + return !rootParameters().isEmpty(); + } + + public boolean isSingle() { + return getReturnType().isScalar() + || getReturnType().isParametrized() + || !getIndex().getClassByName(method.returnType().name()).hasAnnotation(MULTIPLE); + } + + public boolean hasDirectives() { + return !directives.isEmpty(); + } + + public List getDirectives() { + return directives; + } + + public String getMethodIdentifier() { + return getOperationTypeAsString() + "_" + getName(); + } + + public String getOperationTypeAsString() { + switch (getOperationType()) { + case MUTATION: + return "mutation"; + case SUBSCRIPTION: + return "subscription "; + default: + return "query"; + } + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/NamedElement.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/NamedElement.java new file mode 100644 index 000000000..5544059b0 --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/NamedElement.java @@ -0,0 +1,13 @@ +package io.smallrye.graphql.client.model.helper; + +public interface NamedElement { + String getName(); + + String getRawName(); + + String getDirectiveLocation(); + + default boolean isRenamed() { + return !getName().equals(getRawName()); + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java new file mode 100644 index 000000000..5516e03de --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java @@ -0,0 +1,129 @@ +package io.smallrye.graphql.client.model.helper; + +import static io.smallrye.graphql.client.model.Annotations.ID; +import static io.smallrye.graphql.client.model.Annotations.INPUT; +import static io.smallrye.graphql.client.model.Annotations.NAME; +import static io.smallrye.graphql.client.model.Annotations.NESTED_PARAMETER; +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.jboss.jandex.MethodParameterInfo; + +import io.smallrye.graphql.client.model.Scalars; +import io.smallrye.graphql.client.typesafe.api.Header; + +public class ParameterModel implements NamedElement { + private MethodParameterInfo parameter; + private TypeModel type; + private List directives; + + ParameterModel(MethodParameterInfo parameter) { + this.parameter = parameter; + this.type = TypeModel.of(parameter.type()); + this.directives = DirectiveHelper.resolveDirectives(parameter.annotations().stream(), getDirectiveLocation()) + .map(DirectiveInstance::of) + .collect(toList()); + } + + public static ParameterModel of(MethodParameterInfo parameter) { + return new ParameterModel(parameter); + } + + private boolean isHeaderParameter() { + return parameter.hasAnnotation(Header.class); + } + + public boolean isValueParameter() { + return isRootParameter() || isNestedParameter(); + } + + public boolean isRootParameter() { + return !isHeaderParameter() && !isNestedParameter(); + } + + public boolean isNestedParameter() { + return parameter.hasAnnotation(NESTED_PARAMETER); + } + + public Stream getNestedParameterNames() { + return Stream.of(parameter.annotation(NESTED_PARAMETER).value().asStringArray()); + } + + public String getName() { + if (parameter.hasAnnotation(NAME)) + return parameter.annotation(NAME).value().asString(); + if (parameter.name() == null) // ??? + throw new RuntimeException("Missing name information for " + this + ".\n" + + "You can either annotate all parameters with @Name, " + + "or compile your source code with the -parameters options, " + + "so the parameter names are compiled into the class file and available at runtime."); + return getRawName(); + } + + public String getRawName() { + return parameter.name(); + } + + @Override + public String getDirectiveLocation() { + return "VARIABLE_DEFINITION"; + } + + public String graphQlInputTypeName() { + if (parameter.hasAnnotation(ID)) { + if (type.isCollectionOrArray()) { + return "[ID" + arrayOrCollectionHelper(this::optionalExclamationMark) + "]" + optionalExclamationMark(type); + } + return "ID" + optionalExclamationMark(type); + } else if (type.isCollectionOrArray()) { + return "[" + arrayOrCollectionHelper(this::withExclamationMark) + "]" + optionalExclamationMark(type); + } else if (type.isMap()) { + var keyType = type.getMapKeyType(); + var valueType = type.getMapValueType(); + return "[Entry_" + withExclamationMark(keyType) + + "_" + withExclamationMark(valueType) + "Input]" + + optionalExclamationMark(type); + } else { + return withExclamationMark(type); + } + } + + private String withExclamationMark(TypeModel type) { + return graphQlInputTypeName(type) + optionalExclamationMark(type); + } + + private String graphQlInputTypeName(TypeModel type) { + if (type.hasAnnotation(INPUT)) { + String value = type.getAnnotation(INPUT).value().asString(); + if (!value.isEmpty()) { + return value; + } + } + if (type.hasAnnotation(NAME)) + return type.getAnnotation(NAME).value().asString(); + if (Scalars.isScalar(type.getName())) { + return Scalars.getScalar(type.getName()); // returns simplified name + } + return type.getSimpleName() + (type.isEnum() ? "" : "Input"); + } + + // method.returnType() + private String optionalExclamationMark(TypeModel type) { + return type.isNonNull() ? "!" : ""; + } + + private String arrayOrCollectionHelper(Function supplier) { + return supplier.apply((type.isArray() ? type.getArrayElementType() : type.getCollectionElementType())); + } + + public boolean hasDirectives() { + return !directives.isEmpty(); + } + + public List getDirectiveInstances() { + return directives; + } +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeAutoNameStrategy.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeAutoNameStrategy.java new file mode 100644 index 000000000..a3c435aca --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeAutoNameStrategy.java @@ -0,0 +1,12 @@ +package io.smallrye.graphql.client.model.helper; + +/** + * Naming strategy for type + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public enum TypeAutoNameStrategy { + Default, // Spec compliant + MergeInnerClass, // Inner class prefix parent name + Full // Use fully qualified name +} diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java new file mode 100644 index 000000000..e0f394b4a --- /dev/null +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java @@ -0,0 +1,208 @@ +package io.smallrye.graphql.client.model.helper; + +import static io.smallrye.graphql.client.model.Annotations.IGNORE; +import static io.smallrye.graphql.client.model.Annotations.JACKSON_IGNORE; +import static io.smallrye.graphql.client.model.Annotations.JAKARTA_JSONB_TRANSIENT; +import static io.smallrye.graphql.client.model.Classes.ERROR_OR; +import static io.smallrye.graphql.client.model.Classes.OBJECT; +import static io.smallrye.graphql.client.model.Classes.OPTIONAL; +import static io.smallrye.graphql.client.model.Classes.TYPESAFE_RESPONSE; +import static io.smallrye.graphql.client.model.Classes.isParameterized; +import static io.smallrye.graphql.client.model.ScanningContext.getIndex; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.Type; + +import io.smallrye.graphql.client.model.Annotations; +import io.smallrye.graphql.client.model.Classes; +import io.smallrye.graphql.client.model.Scalars; + +public class TypeModel { + private Type type; + + public static TypeModel of(Type type) { + return new TypeModel(type); + } + + TypeModel(Type type) { + this.type = type; + } + + public boolean isPrimitive() { + return Classes.isPrimitive(type); + } + + public boolean isMap() { + return Classes.isMap(type); + } + + public boolean isCollectionOrArray() { + return isArray() || isCollection(); + } + + public boolean isClassType() { + return isSimpleClassType() || isParametrized() || type.name().equals(OBJECT); + } + + public boolean isSimpleClassType() { + return Classes.isClass(type) && !isPrimitive(); + } + + public TypeModel getMapKeyType() { + if (!isMap()) { + throw new IllegalArgumentException("Expected type to be a Map"); + } + return of(type.asParameterizedType().arguments().get(0)); + } + + public TypeModel getMapValueType() { + if (!isMap()) { + throw new IllegalArgumentException("Expected type to be a Map"); + } + return of(type.asParameterizedType().arguments().get(1)); + } + + public boolean isNonNull() { + return isPrimitive() || + type.hasAnnotation(Annotations.NON_NULL) || + type.hasAnnotation(Annotations.JAKARTA_NON_NULL); + } + + public boolean isArray() { + return Classes.isArray(type); + } + + public boolean isCollection() { + return Classes.isCollection(type); + } + + public boolean hasAnnotation(DotName annotation) { + return type.hasAnnotation(annotation); + } + + public AnnotationInstance getAnnotation(DotName annotation) { + return type.annotation(annotation); + } + + public String getSimpleName() { + return type.name().withoutPackagePrefix(); + } + + public String getName() { + String name = type.name().toString(); + if (isParameterized(type)) { + return name + "<" + getItemTypes().map(TypeModel::getName).collect(Collectors.joining(", ")) + ">"; + } + return name; + } + + public Stream getItemTypes() { + if (!Classes.isParameterized(type)) { + throw new IllegalArgumentException("Type " + getName() + " is not parametrized, cannot get its Item Types"); + } + return type.asParameterizedType().arguments().stream().map(TypeModel::of); + } + + public TypeModel getArrayElementType() { + if (!isArray()) { + throw new IllegalArgumentException("Type " + getName() + " is not an array type, cannot get its Element Type"); + } + return of(type.asArrayType().elementType()); + } + + public TypeModel getItemTypeOrElementType() { + if (isArray()) { + return getArrayElementType(); + } + return getFirstRawType(); + } + + public TypeModel getCollectionElementType() { + return getFirstRawType(); + } + + public TypeModel getFirstRawType() { + return getItemTypes().collect(toList()).get(0); + + } + + public boolean isEnum() { + return Classes.isEnum(type); + } + + public boolean isScalar() { + return Scalars.isScalar(getName()) || isEnum(); + } + + public boolean isTypesafeResponse() { + return isParametrized() && type.name().equals(TYPESAFE_RESPONSE); + } + + public boolean isErrorOr() { + return isParametrized() && type.name().equals(ERROR_OR); + } + + public boolean isOptional() { // Optional* classes are considered as Scalars, we check here if type is a `Optional` + return isParametrized() && type.name().equals(OPTIONAL); + } + + public boolean isAsync() { + return Classes.isAsync(type); + } + + public boolean isParametrized() { + return Classes.isParameterized(type); + } + + public boolean isTypeVariable() { + return Classes.isTypeVariable(type); + } + + public Stream fields() { + if (!isClassType()) { + throw new IllegalArgumentException( + "Expected type " + type.name().toString() + " to be Class type, cannot get fields from non-class type"); + } + return fields(getIndex().getClassByName(type.name())); + } + + private Stream fields(ClassInfo clazz) { + return (clazz == null) ? Stream.of() + : Stream.concat( + fields(getIndex().getClassByName(clazz.superClassType().asClassType().name())), // to superClass + fieldsHelper(clazz) + .map(field -> FieldModel.of(field, TypeModel.of(type))) + .filter(this::isGraphQlField)); + + } + + private Stream fieldsHelper(ClassInfo clazz) { + if (System.getSecurityManager() == null) { + return clazz.fields().stream(); + } + return AccessController.doPrivileged((PrivilegedAction>) () -> clazz.fields().stream()); + + } + + private boolean isGraphQlField(FieldModel field) { + return !field.isStatic() && + !field.isSynthetic() && + !field.isTransient() && + !isAnnotatedBy(field, IGNORE, JAKARTA_JSONB_TRANSIENT, JACKSON_IGNORE); + } + + private boolean isAnnotatedBy(FieldModel field, DotName... annotationClasses) { + return Arrays.stream(annotationClasses).anyMatch(field::hasAnnotation); + } +} diff --git a/client/model/pom.xml b/client/model/pom.xml new file mode 100644 index 000000000..0498c068c --- /dev/null +++ b/client/model/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + io.smallrye + smallrye-graphql-client-parent + 2.6.2-SNAPSHOT + + + smallrye-graphql-client-model + SmallRye: GraphQL Client :: model + \ No newline at end of file diff --git a/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java new file mode 100644 index 000000000..c4f4bc6fa --- /dev/null +++ b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java @@ -0,0 +1,51 @@ +package io.smallrye.graphql.client.model; + +import java.util.HashMap; +import java.util.Map; + +public class ClientModels { + + public static class ClientModel { + Map operationMap; + + public ClientModel() { + operationMap = new HashMap<>(); + }; + + public Map getOperationMap() { + return operationMap; + } + + public void setOperationMap(Map operationMap) { + this.operationMap = operationMap; + } + + @Override + public String toString() { + return "ClientModel{" + + "operationMap=" + operationMap + + '}'; + } + } + + private Map clientModelMap; + + public ClientModels() { + } + + public void setClientModelMap(Map clientModelMap) { + this.clientModelMap = clientModelMap; + } + + public Map getClientModelMap() { + return clientModelMap; + } + + // for testing purposes... + @Override + public String toString() { + return "ClientModel{" + + "operationMap=" + clientModelMap.toString() + + '}'; + } +} diff --git a/client/pom.xml b/client/pom.xml index bacc135bb..7c299a94f 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -21,6 +21,8 @@ generator generator-test tck + model + model-builder diff --git a/pom.xml b/pom.xml index ea925f197..fb3a06974 100644 --- a/pom.xml +++ b/pom.xml @@ -352,6 +352,16 @@ smallrye-graphql-client-implementation-vertx ${project.version} + + ${project.groupId} + smallrye-graphql-client-model + ${project.version} + + + ${project.groupId} + smallrye-graphql-client-model-builder + ${project.version} + com.graphql-java diff --git a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java index 0f8454dbd..8780bafd5 100644 --- a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java +++ b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToDynamicClientTest.java @@ -10,7 +10,6 @@ import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,7 +44,6 @@ public void testSimpleRecord() throws Exception { assertEquals("a", result.a()); assertEquals("b", result.b()); } - } /** diff --git a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java index adb092364..7fc7970dd 100644 --- a/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java +++ b/server/integration-tests-jdk16/src/test/java/io/smallrye/graphql/tests/client/RecordAsInputToTypesafeClientTest.java @@ -9,9 +9,7 @@ import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; -import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith;