Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ formatter-maven-cache.properties

# ignore Gradle generated build folders
build
bin

# ignore downloaded maven
/tools/maven
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ private static Map<DotName, AnnotationInstance> getAnnotationsWithFilter(org.jbo
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 RESOLVER = DotName.createSimple("io.smallrye.graphql.api.federation.Resolver");
public static final DotName TARGET = DotName.createSimple("io.smallrye.graphql.api.Target");
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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ private SchemaBuilder(TypeAutoNameStrategy autoNameStrategy) {
referenceCreator = new ReferenceCreator(autoNameStrategy);
fieldCreator = new FieldCreator(referenceCreator);
argumentCreator = new ArgumentCreator(referenceCreator);
inputTypeCreator = new InputTypeCreator(fieldCreator);
operationCreator = new OperationCreator(referenceCreator, argumentCreator);
inputTypeCreator = new InputTypeCreator(fieldCreator, operationCreator);
typeCreator = new TypeCreator(referenceCreator, fieldCreator, operationCreator);
interfaceCreator = new InterfaceCreator(referenceCreator, fieldCreator, operationCreator);
directiveTypeCreator = new DirectiveTypeCreator(referenceCreator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ public Optional<Argument> createArgument(Operation operation, MethodInfo methodI
.orElse(defaultName);

Reference reference;
if (isSourceAnnotationOnSourceOperation(annotationsForThisArgument, operation)) {
boolean sourceArgument = isSourceAnnotationOnSourceOperation(annotationsForThisArgument, operation);

if (sourceArgument) {
reference = referenceCreator.createReferenceForSourceArgument(argumentType, annotationsForThisArgument);
} else if (!argumentType.name().equals(CONTEXT)) {
reference = referenceCreator.createReferenceForOperationArgument(argumentType, annotationsForThisArgument);
Expand All @@ -89,9 +91,7 @@ public Optional<Argument> createArgument(Operation operation, MethodInfo methodI
name,
reference);

if (isSourceAnnotationOnSourceOperation(annotationsForThisArgument, operation)) {
argument.setSourceArgument(true);
}
argument.setSourceArgument(sourceArgument);

if (validationHelper != null) {
List<DirectiveInstance> constraintDirectives = validationHelper
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.smallrye.graphql.schema.creator;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand All @@ -17,6 +18,7 @@
import io.smallrye.graphql.schema.Classes;
import io.smallrye.graphql.schema.SchemaBuilderException;
import io.smallrye.graphql.schema.helper.DeprecatedDirectivesHelper;
import io.smallrye.graphql.schema.helper.DescriptionHelper;
import io.smallrye.graphql.schema.helper.Direction;
import io.smallrye.graphql.schema.helper.MethodHelper;
import io.smallrye.graphql.schema.helper.RolesAllowedDirectivesHelper;
Expand All @@ -25,6 +27,7 @@
import io.smallrye.graphql.schema.model.Operation;
import io.smallrye.graphql.schema.model.OperationType;
import io.smallrye.graphql.schema.model.Reference;
import io.smallrye.graphql.schema.model.Wrapper;
import kotlin.metadata.Attributes;
import kotlin.metadata.KmClassifier;
import kotlin.metadata.KmFunction;
Expand Down Expand Up @@ -113,6 +116,45 @@ public Operation createOperation(MethodInfo methodInfo, OperationType operationT
return operation;
}

/**
* Creates an operation that is exposed as an input field by @Target.
*/
public Operation createTargetOperation(MethodInfo methodInfo, Reference targetFieldOn) {
if (!Modifier.isPublic(methodInfo.flags())) {
throw new IllegalArgumentException(
"Method " + methodInfo.declaringClass().name().toString() + "#" + methodInfo.name()
+ " is used as a target input field, but is not public");
}

short[] parameterPositions = getTargetParameterPositions(methodInfo);
short targetPosition = parameterPositions[0];
short valuePosition = parameterPositions[1];

Type valueType = methodInfo.parameterType(valuePosition);
Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo);
Annotations annotationsForValue = Annotations.getAnnotationsForArgument(methodInfo, valuePosition);

String name = getTargetFieldName(methodInfo, annotationsForMethod);
Reference reference = referenceCreator.createReferenceForOperationArgument(valueType, annotationsForValue);

Operation operation = new Operation(methodInfo.declaringClass().name().toString(),
methodInfo.name(),
name,
name,
reference,
null,
null);
operation.setTargetFieldOn(targetFieldOn);
operation.setParameterClassNames(getParameterClassNames(methodInfo));
operation.setTargetParameterPosition(targetPosition);
operation.setValueParameterPosition(valuePosition);

populateField(Direction.IN, operation, valueType, annotationsForValue);
DescriptionHelper.getDescriptionForField(annotationsForMethod, valueType).ifPresent(operation::setDescription);

return operation;
}

// If the operation return type is a wrapper and is written in Kotlin,
// this checks whether the wrapped type is nullable.
// Nullability metadata is stored in the kotlin.Metadata annotation
Expand Down Expand Up @@ -294,6 +336,51 @@ private static String getOperationName(MethodInfo methodInfo, OperationType oper

}

private short[] getTargetParameterPositions(MethodInfo methodInfo) {
short targetPosition = -1;
short valuePosition = -1;
int targetCount = 0;
int valueCount = 0;
for (short i = 0; i < methodInfo.parametersCount(); i++) {
Annotations annotations = Annotations.getAnnotationsForArgument(methodInfo, i);
if (annotations.containsOneOfTheseAnnotations(Annotations.TARGET)) {
targetPosition = i;
targetCount++;
} else if (!methodInfo.parameterType(i).name().equals(CONTEXT)) {
valuePosition = i;
valueCount++;
}
}
if (targetCount != 1 || valueCount != 1) {
throw new SchemaBuilderException("Target input field method [" + methodInfo.declaringClass().name()
+ "#" + methodInfo.name() + "] must declare exactly one @Target parameter and one input value parameter");
}
return new short[] { targetPosition, valuePosition };
}

private List<String> getParameterClassNames(MethodInfo methodInfo) {
List<String> parameterClassNames = new ArrayList<>();
for (Type parameterType : methodInfo.parameterTypes()) {
parameterClassNames.add(getParameterClassName(parameterType));
}
return parameterClassNames;
}

private String getParameterClassName(Type parameterType) {
return WrapperCreator.createWrapper(parameterType)
.map(Wrapper::getWrapperClassName)
.orElse(parameterType.name().toString());
}

private static String getTargetFieldName(MethodInfo methodInfo, Annotations annotations) {
return annotations.getOneOfTheseMethodAnnotationsValue(
Annotations.NAME,
Annotations.JAKARTA_JSONB_PROPERTY,
Annotations.JAVAX_JSONB_PROPERTY,
Annotations.JACKSON_PROPERTY)
.orElse(methodInfo.name());
}

private static DotName getOperationAnnotation(OperationType operationType) {
switch (operationType) {
case QUERY:
Expand Down Expand Up @@ -372,4 +459,6 @@ private void addDirectiveForDeprecated(Annotations annotationsForOperation, Oper
public String getDirectiveLocation() {
return "FIELD_DEFINITION";
}

private static final DotName CONTEXT = DotName.createSimple("io.smallrye.graphql.api.Context");
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@
import java.util.Optional;

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.logging.Logger;

import io.smallrye.graphql.schema.Annotations;
import io.smallrye.graphql.schema.Classes;
import io.smallrye.graphql.schema.ScanningContext;
import io.smallrye.graphql.schema.SchemaBuilderException;
import io.smallrye.graphql.schema.creator.FieldCreator;
import io.smallrye.graphql.schema.creator.OperationCreator;
import io.smallrye.graphql.schema.helper.DescriptionHelper;
import io.smallrye.graphql.schema.helper.Direction;
import io.smallrye.graphql.schema.helper.Directives;
import io.smallrye.graphql.schema.helper.MethodHelper;
import io.smallrye.graphql.schema.helper.TargetOperationHelper;
import io.smallrye.graphql.schema.helper.TypeNameHelper;
import io.smallrye.graphql.schema.model.DirectiveInstance;
import io.smallrye.graphql.schema.model.Field;
import io.smallrye.graphql.schema.model.InputType;
import io.smallrye.graphql.schema.model.Operation;
import io.smallrye.graphql.schema.model.Reference;
import io.smallrye.graphql.schema.model.ReferenceType;

Expand All @@ -38,10 +44,12 @@
public class InputTypeCreator implements Creator<InputType> {
private static final Logger LOG = Logger.getLogger(InputTypeCreator.class.getName());
private final FieldCreator fieldCreator;
private final OperationCreator operationCreator;
private Directives directives;

public InputTypeCreator(FieldCreator fieldCreator) {
public InputTypeCreator(FieldCreator fieldCreator, OperationCreator operationCreator) {
this.fieldCreator = fieldCreator;
this.operationCreator = operationCreator;
}

@Override
Expand Down Expand Up @@ -186,6 +194,37 @@ private void addFields(InputType inputType, ClassInfo classInfo, Reference refer
}
}

addTargetFields(inputType, classInfo);
}

private void addTargetFields(InputType inputType, ClassInfo classInfo) {
TargetOperationHelper targetOperationHelper = new TargetOperationHelper();
Map<DotName, List<MethodParameterInfo>> targetFields = targetOperationHelper.getTargetAnnotations();
if (!targetFields.containsKey(classInfo.name())) {
return;
}

List<MethodParameterInfo> methodParameterInfos = targetFields.get(classInfo.name());
for (MethodParameterInfo methodParameterInfo : methodParameterInfos) {
MethodInfo methodInfo = methodParameterInfo.method();
Operation targetOperation = operationCreator.createTargetOperation(methodInfo, inputType);
validateTargetFieldName(inputType, targetOperation);
inputType.addTargetField(targetOperation);
}
}

private void validateTargetFieldName(InputType inputType, Operation targetOperation) {
if (inputType.hasField(targetOperation.getName())) {
throw new SchemaBuilderException(String.format("Input type '%s' already contains field named '%s'" +
" so target input field, with the same name, cannot be applied. You can resolve this conflict using @Name on the target method.",
inputType.getName(),
targetOperation.getName()));
}
if (inputType.hasTargetField(targetOperation.getName())) {
throw new SchemaBuilderException(String.format("Input type '%s' already contains target input field named '%s'",
inputType.getName(),
targetOperation.getName()));
}
}

private static final String JAVA_DOT = "java.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.smallrye.graphql.schema.helper;

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.AnnotationTarget;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.smallrye.graphql.schema.Annotations;
import io.smallrye.graphql.schema.ScanningContext;

/**
* Finds all @Target input fields.
*/
public class TargetOperationHelper {
private final Logger LOG = Logger.getLogger(TargetOperationHelper.class.getName());

private final Map<DotName, List<MethodParameterInfo>> targetAnnotations = scanAllTargetAnnotations();

public Map<DotName, List<MethodParameterInfo>> getTargetAnnotations() {
return targetAnnotations;
}

private Map<DotName, List<MethodParameterInfo>> scanAllTargetAnnotations() {
Map<DotName, List<MethodParameterInfo>> targetFields = new HashMap<>();
Collection<AnnotationInstance> targetAnnotations = ScanningContext.getIndex().getAnnotations(Annotations.TARGET);
for (AnnotationInstance ai : targetAnnotations) {
AnnotationTarget target = ai.target();
if (target.kind().equals(AnnotationTarget.Kind.METHOD_PARAMETER)) {
MethodParameterInfo methodParameter = target.asMethodParameter();
Type targetType = methodParameter.method().parameterType(methodParameter.position());
DotName name = getName(targetType);
targetFields.computeIfAbsent(name, k -> new ArrayList<>()).add(methodParameter);
} else {
LOG.warn("Ignoring " + ai.target() + " on kind " + ai.target().kind() + ". Only expecting @"
+ Annotations.TARGET.local() + " on Method parameters");
}
}
return targetFields;
}

private DotName getName(Type targetType) {
if (targetType.kind().equals(Type.Kind.PARAMETERIZED_TYPE)) {
return targetType.asParameterizedType().name();
}
return targetType.name();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ public void testSchemaWithBatchSourceFieldNameDuplicates() {
}
}

@Test
public void testSchemaWithTargetInputFieldNameDuplicates() {
try {
Indexer indexer = new Indexer();
indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/target");
indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/target/targetfield");
IndexView index = indexer.complete();
SchemaBuilder.build(index);
Assertions.fail(
"Schema should not build when there are both input field and target input field with the same defined name");
} catch (SchemaBuilderException e) {
// ok
assertEquals("Input type 'SomeInputInput' already contains field named 'password'" +
" so target input field, with the same name, cannot be applied. You can resolve this conflict using @Name on the target method.",
e.getMessage());
}
}

@Test
public void testSchemaWithInheritFieldBySubtype() {
Indexer indexer = new Indexer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public final class InputType extends Reference {

private Map<String, Field> fields = new LinkedHashMap<>();

private Map<String, Operation> targetFields = new LinkedHashMap<>();

/**
* All fields which are required by the creator method (e.g. {@code @JsonbCreator}).
*/
Expand Down Expand Up @@ -61,6 +63,26 @@ public boolean hasField(String fieldName) {
return this.fields.containsKey(fieldName);
}

public Map<String, Operation> getTargetFields() {
return targetFields;
}

public void setTargetFields(Map<String, Operation> targetFields) {
this.targetFields = targetFields;
}

public void addTargetField(Operation targetField) {
this.targetFields.put(targetField.getName(), targetField);
}

public boolean hasTargetFields() {
return !this.targetFields.isEmpty();
}

public boolean hasTargetField(String fieldName) {
return this.targetFields.containsKey(fieldName);
}

public List<Field> getCreatorParameters() {
return creatorParameters;
}
Expand All @@ -75,8 +97,8 @@ public void addCreatorParameter(Field creatorParameter) {

@Override
public String toString() {
return "InputType{" + "description=" + description + ", fields=" + fields + ", creatorParameters=" + creatorParameters
+ '}';
return "InputType{" + "description=" + description + ", fields=" + fields + ", targetFields="
+ targetFields + ", creatorParameters=" + creatorParameters + '}';
}

}
Loading