graphql.schema.SchemaTransformer Maven / Gradle / Ivy
package graphql.schema;
import graphql.Assert;
import graphql.PublicApi;
import graphql.introspection.Introspection;
import graphql.util.Breadcrumb;
import graphql.util.NodeAdapter;
import graphql.util.NodeLocation;
import graphql.util.NodeZipper;
import graphql.util.TraversalControl;
import graphql.util.Traverser;
import graphql.util.TraverserContext;
import graphql.util.TraverserVisitor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static graphql.Assert.assertNotEmpty;
import static graphql.Assert.assertNotNull;
import static graphql.Assert.assertShouldNeverHappen;
import static graphql.schema.GraphQLSchemaElementAdapter.SCHEMA_ELEMENT_ADAPTER;
import static graphql.schema.SchemaElementChildrenContainer.newSchemaElementChildrenContainer;
import static graphql.util.NodeZipper.ModificationType.REPLACE;
import static java.lang.String.format;
/**
* Transforms a {@link GraphQLSchema} object.
*/
@PublicApi
public class SchemaTransformer {
// artificial schema element which serves as root element for the transformation
private static class DummyRoot implements GraphQLSchemaElement {
static final String QUERY = "query";
static final String MUTATION = "mutation";
static final String SUBSCRIPTION = "subscription";
static final String ADD_TYPES = "addTypes";
static final String DIRECTIVES = "directives";
static final String SCHEMA_DIRECTIVES = "schemaDirectives";
static final String INTROSPECTION = "introspection";
GraphQLSchema schema;
GraphQLObjectType query;
GraphQLObjectType mutation;
GraphQLObjectType subscription;
Set additionalTypes;
Set directives;
Set schemaDirectives;
DummyRoot(GraphQLSchema schema) {
this.schema = schema;
query = schema.getQueryType();
mutation = schema.isSupportingMutations() ? schema.getMutationType() : null;
subscription = schema.isSupportingSubscriptions() ? schema.getSubscriptionType() : null;
additionalTypes = schema.getAdditionalTypes();
schemaDirectives = new LinkedHashSet<>(schema.getSchemaDirectives());
directives = new LinkedHashSet<>(schema.getDirectives());
}
@Override
public List getChildren() {
return assertShouldNeverHappen();
}
@Override
public SchemaElementChildrenContainer getChildrenWithTypeReferences() {
SchemaElementChildrenContainer.Builder builder = newSchemaElementChildrenContainer()
.child(QUERY, query);
if (schema.isSupportingMutations()) {
builder.child(MUTATION, mutation);
}
if (schema.isSupportingSubscriptions()) {
builder.child(SUBSCRIPTION, subscription);
}
builder.children(ADD_TYPES, additionalTypes);
builder.children(DIRECTIVES, directives);
builder.children(SCHEMA_DIRECTIVES, schemaDirectives);
builder.child(INTROSPECTION, Introspection.__Schema);
return builder.build();
}
@Override
public GraphQLSchemaElement withNewChildren(SchemaElementChildrenContainer newChildren) {
// special hack: we don't create a new dummy root, but we simply update it
query = newChildren.getChildOrNull(QUERY);
mutation = newChildren.getChildOrNull(MUTATION);
subscription = newChildren.getChildOrNull(SUBSCRIPTION);
additionalTypes = new LinkedHashSet<>(newChildren.getChildren(ADD_TYPES));
directives = new LinkedHashSet<>(newChildren.getChildren(DIRECTIVES));
schemaDirectives = new LinkedHashSet<>(newChildren.getChildren(SCHEMA_DIRECTIVES));
return this;
}
@Override
public TraversalControl accept(TraverserContext context, GraphQLTypeVisitor visitor) {
return assertShouldNeverHappen();
}
}
/**
* Transforms a GraphQLSchema and returns a new GraphQLSchema object.
*
* @param schema the schema to transform
* @param visitor the visitor call back
* @return a new GraphQLSchema instance.
*/
public static GraphQLSchema transformSchema(GraphQLSchema schema, GraphQLTypeVisitor visitor) {
SchemaTransformer schemaTransformer = new SchemaTransformer();
return schemaTransformer.transform(schema, visitor);
}
public GraphQLSchema transform(final GraphQLSchema schema, GraphQLTypeVisitor visitor) {
DummyRoot dummyRoot = new DummyRoot(schema);
List> zippers = new LinkedList<>();
Map> zipperByNodeAfterTraversing = new LinkedHashMap<>();
Map> zipperByOriginalNode = new LinkedHashMap<>();
Map, List>>> breadcrumbsByZipper = new LinkedHashMap<>();
Map> reverseDependencies = new LinkedHashMap<>();
TraverserVisitor nodeTraverserVisitor = new TraverserVisitor() {
@Override
public TraversalControl enter(TraverserContext context) {
if (context.thisNode() == dummyRoot) {
return TraversalControl.CONTINUE;
}
NodeZipper nodeZipper = new NodeZipper<>(context.thisNode(), context.getBreadcrumbs(), SCHEMA_ELEMENT_ADAPTER);
context.setVar(NodeZipper.class, nodeZipper);
context.setVar(NodeAdapter.class, SCHEMA_ELEMENT_ADAPTER);
int zippersBefore = zippers.size();
TraversalControl result = context.thisNode().accept(context, visitor);
// detection if the node was changed
if (zippersBefore + 1 == zippers.size()) {
nodeZipper = zippers.get(zippers.size() - 1);
}
zipperByOriginalNode.put(context.originalThisNode(), nodeZipper);
if (context.isDeleted()) {
zipperByNodeAfterTraversing.put(context.originalThisNode(), nodeZipper);
} else {
zipperByNodeAfterTraversing.put(context.thisNode(), nodeZipper);
}
breadcrumbsByZipper.put(nodeZipper, new ArrayList<>());
breadcrumbsByZipper.get(nodeZipper).add(context.getBreadcrumbs());
if (nodeZipper.getModificationType() != NodeZipper.ModificationType.DELETE) {
reverseDependencies.computeIfAbsent(context.thisNode(), ign -> new ArrayList<>()).add(context.getParentNode());
}
return result;
}
@Override
public TraversalControl leave(TraverserContext context) {
return TraversalControl.CONTINUE;
}
@Override
public TraversalControl backRef(TraverserContext context) {
NodeZipper zipper = zipperByOriginalNode.get(context.thisNode());
breadcrumbsByZipper.get(zipper).add(context.getBreadcrumbs());
visitor.visitBackRef(context);
List reverseDependenciesForCurNode = reverseDependencies.get(zipper.getCurNode());
assertNotNull(reverseDependenciesForCurNode);
reverseDependenciesForCurNode.add(context.getParentNode());
return TraversalControl.CONTINUE;
}
};
Traverser traverser = Traverser.depthFirstWithNamedChildren(SCHEMA_ELEMENT_ADAPTER::getNamedChildren, zippers, null);
GraphQLCodeRegistry.Builder builder = GraphQLCodeRegistry.newCodeRegistry(schema.getCodeRegistry());
traverser.rootVar(GraphQLCodeRegistry.Builder.class, builder);
traverser.traverse(dummyRoot, nodeTraverserVisitor);
List topologicalSort = topologicalSort(zipperByNodeAfterTraversing.keySet(), reverseDependencies);
zipUpToDummyRoot(zippers, topologicalSort, breadcrumbsByZipper, zipperByNodeAfterTraversing);
GraphQLSchema newSchema = GraphQLSchema.newSchema()
.query(dummyRoot.query)
.mutation(dummyRoot.mutation)
.subscription(dummyRoot.subscription)
.additionalTypes(dummyRoot.additionalTypes)
.additionalDirectives(dummyRoot.directives)
.withSchemaDirectives(dummyRoot.schemaDirectives)
.codeRegistry(builder.build())
.description(schema.getDescription())
.buildImpl(true);
return newSchema;
}
private List topologicalSort(Set allNodes, Map> reverseDependencies) {
List result = new ArrayList<>();
Set notPermMarked = new LinkedHashSet<>(allNodes);
Set tempMarked = new LinkedHashSet<>();
Set permMarked = new LinkedHashSet<>();
/**
* Taken from: https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
* while exists nodes without a permanent mark do
* select an unmarked node n
* visit(n)
*/
while (true) {
Iterator iterator = notPermMarked.iterator();
if (!iterator.hasNext()) {
break;
}
GraphQLSchemaElement n = iterator.next();
iterator.remove();
visit(n, tempMarked, permMarked, notPermMarked, result, reverseDependencies);
}
return result;
}
private void visit(GraphQLSchemaElement n,
Set tempMarked,
Set permMarked,
Set notPermMarked,
List result,
Map> reverseDependencies) {
/**
* Taken from: https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
* if n has a permanent mark then
* return
* if n has a temporary mark then
* stop (not a DAG)
*
* mark n with a temporary mark
*
* for each node m with an edge from n to m do
* visit(m)
*
* remove temporary mark from n
* mark n with a permanent mark
* add n to head of L
*/
if (permMarked.contains(n)) {
return;
}
if (tempMarked.contains(n)) {
Assert.assertShouldNeverHappen("NOT A DAG: %s has temp mark", n);
return;
}
tempMarked.add(n);
if (reverseDependencies.containsKey(n)) {
for (GraphQLSchemaElement m : reverseDependencies.get(n)) {
visit(m, tempMarked, permMarked, notPermMarked, result, reverseDependencies);
}
}
tempMarked.remove(n);
permMarked.add(n);
notPermMarked.remove(n);
result.add(n);
}
private void zipUpToDummyRoot(List> zippers,
List topSort,
Map, List>>> breadcrumbsByZipper,
Map> nodeToZipper) {
if (zippers.size() == 0) {
return;
}
Set> curZippers = new LinkedHashSet<>(zippers);
for (int i = topSort.size() - 1; i >= 0; i--) {
GraphQLSchemaElement element = topSort.get(i);
// that the map goes from zipper -> one List (= one path) is because we know that in a schema one element
// has never two different edges to another element
Map, List>> zipperWithSameParent = zipperWithSameParent(element, curZippers, breadcrumbsByZipper);
// this means we have a node which doesn't need to be changed
if (zipperWithSameParent.size() == 0) {
continue;
}
NodeZipper newZipper = moveUp(element, zipperWithSameParent);
if (element instanceof DummyRoot) {
// this means we have updated the dummy root and we are done (dummy root is a special as it gets updated in place, see Implementation of DummyRoot)
break;
}
// update curZippers
NodeZipper curZipperForElement = nodeToZipper.get(element);
assertNotNull(curZipperForElement, () -> format("curZipperForElement is null for parentNode %s", element));
curZippers.remove(curZipperForElement);
curZippers.add(newZipper);
// update breadcrumbsByZipper to use the newZipper
List>> breadcrumbsForOriginalParent = breadcrumbsByZipper.get(curZipperForElement);
assertNotNull(breadcrumbsForOriginalParent, () -> format("No breadcrumbs found for zipper %s", curZipperForElement));
breadcrumbsByZipper.remove(curZipperForElement);
breadcrumbsByZipper.put(newZipper, breadcrumbsForOriginalParent);
}
}
private Map, List>> zipperWithSameParent(GraphQLSchemaElement parent,
Set> zippers,
Map, List>>> curBreadcrumbsByZipper) {
Map, List>> result = new LinkedHashMap<>();
outer:
for (NodeZipper zipper : zippers) {
for (List> path : curBreadcrumbsByZipper.get(zipper)) {
if (path.get(0).getNode() == parent) {
result.put(zipper, path);
continue outer;
}
}
}
return result;
}
private static class ZipperWithOneParent {
public ZipperWithOneParent(NodeZipper zipper, Breadcrumb parent) {
this.zipper = zipper;
this.parent = parent;
}
public NodeZipper zipper;
public Breadcrumb parent;
}
private NodeZipper moveUp(
GraphQLSchemaElement parent,
Map, List>> sameParentsZipper) {
Set> sameParent = sameParentsZipper.keySet();
assertNotEmpty(sameParent, () -> "expected at least one zipper");
Map> childrenMap = new HashMap<>(SCHEMA_ELEMENT_ADAPTER.getNamedChildren(parent));
Map indexCorrection = new HashMap<>();
List zipperWithOneParents = new ArrayList<>();
for (NodeZipper zipper : sameParent) {
List> breadcrumbs = sameParentsZipper.get(zipper);
zipperWithOneParents.add(new ZipperWithOneParent(zipper, breadcrumbs.get(0)));
}
zipperWithOneParents.sort((zipperWithOneParent1, zipperWithOneParent2) -> {
NodeZipper zipper1 = zipperWithOneParent1.zipper;
NodeZipper zipper2 = zipperWithOneParent2.zipper;
Breadcrumb breadcrumb1 = zipperWithOneParent1.parent;
Breadcrumb breadcrumb2 = zipperWithOneParent2.parent;
int index1 = breadcrumb1.getLocation().getIndex();
int index2 = breadcrumb2.getLocation().getIndex();
if (index1 != index2) {
return Integer.compare(index1, index2);
}
NodeZipper.ModificationType modificationType1 = zipper1.getModificationType();
NodeZipper.ModificationType modificationType2 = zipper2.getModificationType();
// same index can never be deleted and changed at the same time
if (modificationType1 == modificationType2) {
return 0;
}
// always first replacing the node
if (modificationType1 == REPLACE) {
return -1;
}
// and then INSERT_BEFORE before INSERT_AFTER
return modificationType1 == NodeZipper.ModificationType.INSERT_BEFORE ? -1 : 1;
});
for (ZipperWithOneParent zipperWithOneParent : zipperWithOneParents) {
NodeZipper zipper = zipperWithOneParent.zipper;
Breadcrumb breadcrumb = zipperWithOneParent.parent;
NodeLocation location = breadcrumb.getLocation();
Integer ixDiff = indexCorrection.getOrDefault(location.getName(), 0);
int ix = location.getIndex() + ixDiff;
String name = location.getName();
List childList = new ArrayList<>(childrenMap.get(name));
switch (zipper.getModificationType()) {
case REPLACE:
childList.set(ix, zipper.getCurNode());
break;
case DELETE:
childList.remove(ix);
indexCorrection.put(name, ixDiff - 1);
break;
case INSERT_BEFORE:
childList.add(ix, zipper.getCurNode());
indexCorrection.put(name, ixDiff + 1);
break;
case INSERT_AFTER:
childList.add(ix + 1, zipper.getCurNode());
indexCorrection.put(name, ixDiff + 1);
break;
}
childrenMap.put(name, childList);
}
GraphQLSchemaElement newNode = SCHEMA_ELEMENT_ADAPTER.withNewChildren(parent, childrenMap);
final List> oldBreadcrumbs = sameParent.iterator().next().getBreadcrumbs();
List> newBreadcrumbs;
if (oldBreadcrumbs.size() > 1) {
newBreadcrumbs = oldBreadcrumbs.subList(1, oldBreadcrumbs.size());
} else {
newBreadcrumbs = Collections.emptyList();
}
return new NodeZipper<>(newNode, newBreadcrumbs, SCHEMA_ELEMENT_ADAPTER);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy