Skip to content
Merged
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
48 changes: 48 additions & 0 deletions src/main/java/graphql/annotations/ExtensionDataFetcherWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright 2016 Yurii Rashkovskii
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
*/
package graphql.annotations;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;

import java.util.Map;

import static graphql.annotations.ReflectionKit.newInstance;

public class ExtensionDataFetcherWrapper<T> implements DataFetcher<T>{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey, can you explain to me the need of an ExtensionDataFetcher? I didnt quite understand the purpose of it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - the idea was to be able to use the standard FieldDataFetcher and PropertyDataFetcher on extension objects. When using extensions, the source object is still the extended object (in previous example, instance of A), but the fields and getters are on the extension object (class B). The ExtensionDataFetcher is calling the underlying DataFetcher (Property or Field) with an instance of B as the source object.

MethodDataFetcher is already handling that directly (it's part of graphql-java-annotations, as opposed to the Field and Property DataFetcher). It's checking that method.getDeclaringClass().isInstance(environment.getSource()), and otherwise tries to build an instance of B, or call static methods on B.

Note that it works only if B has a constructor taking A as a parameter. Otherwise, FieldDataFetcher and PropertyDataFetcher won't be able to retrieve any value.

You can look at the example in GraphQLExtensionsTest / TestObjectExtension class.

Of course, if we agree on the way to do it, this will need to be documented :-)


private final Class declaringClass;
private final DataFetcher<T> dataFetcher;

public ExtensionDataFetcherWrapper(Class declaringClass, DataFetcher<T> dataFetcher) {
this.declaringClass = declaringClass;
this.dataFetcher = dataFetcher;
}

@SuppressWarnings("unchecked")
@Override
public T get(DataFetchingEnvironment environment) {
Object source = environment.getSource();
if (source != null && (!declaringClass.isInstance(source)) && !(source instanceof Map)) {
environment = new DataFetchingEnvironmentImpl(newInstance(declaringClass, source), environment.getArguments(),
environment.getContext(), environment.getFields(), environment.getFieldType(), environment.getParentType(),
environment.getGraphQLSchema(), environment.getFragmentsByName(), environment.getExecutionId(), environment.getSelectionSet());
}

return dataFetcher.get(environment);
}

}
127 changes: 84 additions & 43 deletions src/main/java/graphql/annotations/GraphQLAnnotations.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,14 @@

import graphql.TypeResolutionEnvironment;
import graphql.relay.Relay;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import graphql.schema.FieldDataFetcher;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLInputType;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLList;
import graphql.schema.*;
import graphql.schema.GraphQLNonNull;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLTypeReference;
import graphql.schema.GraphQLUnionType;
import graphql.schema.PropertyDataFetcher;
import graphql.schema.TypeResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.validation.constraints.NotNull;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
import java.util.TreeMap;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -83,6 +51,7 @@ public class GraphQLAnnotations implements GraphQLAnnotationsProcessor {
private static final Relay RELAY_TYPES = new Relay();

private Map<String, graphql.schema.GraphQLType> typeRegistry = new HashMap<>();
private Map<Class<?>, Set<Class<?>>> extensionsTypeRegistry = new HashMap<>();
private final Stack<String> processing = new Stack<>();

public GraphQLAnnotations() {
Expand Down Expand Up @@ -180,13 +149,18 @@ public GraphQLInterfaceType.Builder getIfaceBuilder(Class<?> iface) throws Graph
if (description != null) {
builder.description(description.value());
}
List<String> definedFields = new ArrayList<>();
for (Method method : getOrderedMethods(iface)) {
boolean valid = !Modifier.isStatic(method.getModifiers()) &&
method.getAnnotation(GraphQLField.class) != null;
if (valid) {
builder.field(getField(method));
GraphQLFieldDefinition gqlField = getField(method);
definedFields.add(gqlField.getName());
builder.field(gqlField);
}
}
builder.fields(getExtensionFields(iface, definedFields));

GraphQLTypeResolver typeResolver = iface.getAnnotation(GraphQLTypeResolver.class);
builder.typeResolver(newInstance(typeResolver.value()));
return builder;
Expand Down Expand Up @@ -321,13 +295,15 @@ public GraphQLObjectType.Builder getObjectBuilder(Class<?> object) throws GraphQ
if (description != null) {
builder.description(description.value());
}

List<String> fieldsDefined = new ArrayList<>();
for (Method method : getOrderedMethods(object)) {
if (method.isBridge() || method.isSynthetic()) {
continue;
}
if (breadthFirstSearch(method)) {
builder.field(getField(method));
GraphQLFieldDefinition gqlField = getField(method);
fieldsDefined.add(gqlField.getName());
builder.field(gqlField);
}
}

Expand All @@ -336,18 +312,58 @@ public GraphQLObjectType.Builder getObjectBuilder(Class<?> object) throws GraphQ
continue;
}
if (parentalSearch(field)) {
builder.field(getField(field));
GraphQLFieldDefinition gqlField = getField(field);
fieldsDefined.add(gqlField.getName());
builder.field(gqlField);
}
}

for (Class<?> iface : object.getInterfaces()) {
if (iface.getAnnotation(GraphQLTypeResolver.class) != null) {
builder.withInterface((GraphQLInterfaceType) getInterface(iface));
builder.fields(getExtensionFields(iface, fieldsDefined));
}
}

builder.fields(getExtensionFields(object, fieldsDefined));

return builder;
}

private List<GraphQLFieldDefinition> getExtensionFields(Class<?> object, List<String> fieldsDefined) {
List<GraphQLFieldDefinition> fields = new ArrayList<>();
if (extensionsTypeRegistry.containsKey(object)) {
for (Class<?> aClass : extensionsTypeRegistry.get(object)) {
for (Method method : getOrderedMethods(aClass)) {
if (method.isBridge() || method.isSynthetic()) {
continue;
}
if (breadthFirstSearch(method)) {
addExtensionField(getField(method), fields, fieldsDefined);
}
}
for (Field field : getAllFields(aClass).values()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
if (parentalSearch(field)) {
addExtensionField(getField(field), fields, fieldsDefined);
}
}
}
}
return fields;
}

private void addExtensionField(GraphQLFieldDefinition gqlField, List<GraphQLFieldDefinition> fields, List<String> fieldsDefined) {
if (!fieldsDefined.contains(gqlField.getName())) {
fieldsDefined.add(gqlField.getName());
fields.add(gqlField);
} else {
throw new GraphQLAnnotationsException("Duplicate field found in extension : " + gqlField.getName(), null);
}
}

public static GraphQLObjectType.Builder objectBuilder(Class<?> object) throws GraphQLAnnotationsException {
return getInstance().getObjectBuilder(object);
}
Expand Down Expand Up @@ -434,16 +450,16 @@ protected GraphQLFieldDefinition getField(Field field) throws GraphQLAnnotations
if (outputType == GraphQLBoolean || (outputType instanceof GraphQLNonNull && ((GraphQLNonNull) outputType).getWrappedType() == GraphQLBoolean)) {
if (checkIfPrefixGetterExists(field.getDeclaringClass(), "is", field.getName()) ||
checkIfPrefixGetterExists(field.getDeclaringClass(), "get", field.getName())) {
actualDataFetcher = new PropertyDataFetcher(field.getName());
actualDataFetcher = new ExtensionDataFetcherWrapper(field.getDeclaringClass(), new PropertyDataFetcher(field.getName()));
}
} else if (checkIfPrefixGetterExists(field.getDeclaringClass(), "get", field.getName())) {
actualDataFetcher = new PropertyDataFetcher(field.getName());
actualDataFetcher = new ExtensionDataFetcherWrapper(field.getDeclaringClass(), new PropertyDataFetcher(field.getName()));
} else if (hasFluentGetter) {
actualDataFetcher = new MethodDataFetcher(fluentMethod, typeFunction);
}

if (actualDataFetcher == null) {
actualDataFetcher = new FieldDataFetcher(field.getName());
actualDataFetcher = new ExtensionDataFetcherWrapper(field.getDeclaringClass(), new FieldDataFetcher(field.getName()));
}
}

Expand All @@ -459,7 +475,7 @@ protected GraphQLFieldDefinition getField(Field field) throws GraphQLAnnotations

private DataFetcher constructDataFetcher(String fieldName, GraphQLDataFetcher annotatedDataFetcher) {
final String[] args;
if ( annotatedDataFetcher.firstArgIsTargetName() ) {
if (annotatedDataFetcher.firstArgIsTargetName()) {
args = Stream.concat(Stream.of(fieldName), stream(annotatedDataFetcher.args())).toArray(String[]::new);
} else {
args = annotatedDataFetcher.args();
Expand Down Expand Up @@ -673,6 +689,31 @@ public void setDefaultTypeFunction(TypeFunction function) {
((DefaultTypeFunction) defaultTypeFunction).setAnnotationsProcessor(this);
}

public void registerTypeExtension(Class<?> objectClass) {
GraphQLTypeExtension typeExtension = objectClass.getAnnotation(GraphQLTypeExtension.class);
if (typeExtension == null) {
throw new GraphQLAnnotationsException("Class is not annotated with GraphQLTypeExtension", null);
} else {
Class<?> aClass = typeExtension.value();
if (!extensionsTypeRegistry.containsKey(aClass)) {
extensionsTypeRegistry.put(aClass, new HashSet<>());
}
extensionsTypeRegistry.get(aClass).add(objectClass);
}
}

public void unregisterTypeExtension(Class<?> objectClass) {
GraphQLTypeExtension typeExtension = objectClass.getAnnotation(GraphQLTypeExtension.class);
if (typeExtension == null) {
throw new GraphQLAnnotationsException("Class is not annotated with GraphQLTypeExtension", null);
} else {
Class<?> aClass = typeExtension.value();
if (extensionsTypeRegistry.containsKey(aClass)) {
extensionsTypeRegistry.get(aClass).remove(objectClass);
}
}
}

public void registerType(TypeFunction typeFunction) {
((DefaultTypeFunction) defaultTypeFunction).register(typeFunction);
}
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/graphql/annotations/GraphQLTypeExtension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright 2016 Yurii Rashkovskii
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
*/
package graphql.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface GraphQLTypeExtension {
Class<?> value();
}
8 changes: 5 additions & 3 deletions src/main/java/graphql/annotations/MethodDataFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ public Object get(DataFetchingEnvironment environment) {

if (Modifier.isStatic(method.getModifiers())) {
obj = null;
} else if (method.getAnnotation(GraphQLInvokeDetached.class) == null) {
} else if (method.getAnnotation(GraphQLInvokeDetached.class) != null) {
obj = newInstance(method.getDeclaringClass());
} else if (!method.getDeclaringClass().isInstance(environment.getSource())) {
obj = newInstance(method.getDeclaringClass(), environment.getSource());
} else {
obj = environment.getSource();
if (obj == null) {
return null;
}
} else {
obj = newInstance(method.getDeclaringClass());
}
return method.invoke(obj, invocationArgs(environment));
} catch (IllegalAccessException | InvocationTargetException e) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/graphql/annotations/ReflectionKit.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,16 @@ static <T> Constructor<T> constructor(Class<T> type, Class<?>... parameterTypes)
}
}

static <T> T newInstance(Class<T> clazz, Object parameter) {
if (parameter != null) {
for (Constructor<T> constructor : (Constructor<T>[]) clazz.getConstructors()) {
if (constructor.getParameterCount() == 1 && constructor.getParameters()[0].getType().isAssignableFrom(parameter.getClass())) {
return constructNewInstance(constructor, parameter);
}
}
}
return null;
}


}
Loading