All Downloads are FREE. Search and download functionalities are using the official Maven repository.

graphql.schema.SchemaTransformer Maven / Gradle / Ivy

There is a newer version: 230521-nf-execution
Show newest version
package graphql.schema;

import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import graphql.PublicApi;
import graphql.collect.ImmutableKit;
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.Collection;
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 java.util.function.Consumer;

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.schema.impl.StronglyConnectedComponentsTopologicallySorted.getStronglyConnectedComponentsTopologicallySorted;
import static graphql.util.NodeZipper.ModificationType.DELETE;
import static graphql.util.NodeZipper.ModificationType.REPLACE;
import static graphql.util.TraversalControl.CONTINUE;
import static java.lang.String.format;

/**
 * Transforms a {@link GraphQLSchema} object by calling bac on a provided visitor.
 * 

* To change a {@link GraphQLSchemaElement} node in the schema you need * to return {@link GraphQLTypeVisitor#changeNode(TraverserContext, GraphQLSchemaElement)} * which instructs the schema transformer to change that element upon leaving that * visitor method. *

 * {@code
 *  public TraversalControl visitGraphQLObjectType(GraphQLObjectType objectType, TraverserContext context) {
 *      GraphQLObjectType newObjectType = mkSomeNewNode(objectType);
 *      return changeNode(context, newObjectType);
 *  }
 *  }
 * 
*

* To delete an element use {@link GraphQLTypeVisitor#deleteNode(TraverserContext)} *

 * {@code
 *  public TraversalControl visitGraphQLObjectType(GraphQLObjectType objectType, TraverserContext context) {
 *      return deleteNode(context, objectType);
 *  }
 *  }
 * 
*

* To insert elements use either {@link GraphQLTypeVisitor#insertAfter(TraverserContext, GraphQLSchemaElement)} or * {@link GraphQLTypeVisitor#insertBefore(TraverserContext, GraphQLSchemaElement)} * which will insert the new node before or after the current node being visited *

 * {@code
 *  public TraversalControl visitGraphQLObjectType(GraphQLObjectType objectType, TraverserContext context) {
 *      GraphQLObjectType newObjectType = mkSomeNewNode();
 *      return insertAfter(context, newObjectType);
 *  }
 *  }
 * 
*/ @PublicApi public class SchemaTransformer { /** * 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); } /** * Transforms a GraphQLSchema and returns a new GraphQLSchema object. * * @param schema the schema to transform * @param visitor the visitor call back * @param postTransformation a callback that can be as a final step to the schema * * @return a new GraphQLSchema instance. */ public static GraphQLSchema transformSchema(GraphQLSchema schema, GraphQLTypeVisitor visitor, Consumer postTransformation) { SchemaTransformer schemaTransformer = new SchemaTransformer(); return schemaTransformer.transform(schema, visitor, postTransformation); } /** * Transforms a {@link GraphQLSchemaElement} and returns a new element. * * @param schemaElement the schema element to transform * @param visitor the visitor call back * @param for two * * @return a new GraphQLSchemaElement instance. */ public static T transformSchema(final T schemaElement, GraphQLTypeVisitor visitor) { SchemaTransformer schemaTransformer = new SchemaTransformer(); return schemaTransformer.transform(schemaElement, visitor); } public GraphQLSchema transform(final GraphQLSchema schema, GraphQLTypeVisitor visitor) { return (GraphQLSchema) transformImpl(schema, null, visitor, null); } public GraphQLSchema transform(final GraphQLSchema schema, GraphQLTypeVisitor visitor, Consumer postTransformation) { return (GraphQLSchema) transformImpl(schema, null, visitor, postTransformation); } public T transform(final T schemaElement, GraphQLTypeVisitor visitor) { //noinspection unchecked return (T) transformImpl(null, schemaElement, visitor, null); } private Object transformImpl(final GraphQLSchema schema, GraphQLSchemaElement schemaElement, GraphQLTypeVisitor visitor, Consumer postTransformation) { DummyRoot dummyRoot; GraphQLCodeRegistry.Builder codeRegistry = null; if (schema != null) { dummyRoot = new DummyRoot(schema); codeRegistry = GraphQLCodeRegistry.newCodeRegistry(schema.getCodeRegistry()); } else { dummyRoot = new DummyRoot(schemaElement); } final Map changedTypes = new LinkedHashMap<>(); final Map typeReferences = new LinkedHashMap<>(); // first pass - general transformation boolean schemaChanged = traverseAndTransform(dummyRoot, changedTypes, typeReferences, visitor, codeRegistry, schema); // if we have changed any named elements AND we have type references referring to them then // we need to make a second pass to replace these type references to the new names if (!changedTypes.isEmpty()) { boolean hasTypeRefsForChangedTypes = changedTypes.keySet().stream().anyMatch(typeReferences::containsKey); if (hasTypeRefsForChangedTypes) { replaceTypeReferences(dummyRoot, schema, codeRegistry, changedTypes); } } if (schema != null) { GraphQLSchema graphQLSchema = schema; if (schemaChanged || codeRegistry.hasChanged()) { graphQLSchema = dummyRoot.rebuildSchema(codeRegistry); if (postTransformation != null) { graphQLSchema = graphQLSchema.transform(postTransformation); } } return graphQLSchema; } else { return dummyRoot.schemaElement; } } private void replaceTypeReferences(DummyRoot dummyRoot, GraphQLSchema schema, GraphQLCodeRegistry.Builder codeRegistry, Map changedTypes) { GraphQLTypeVisitor typeRefVisitor = new GraphQLTypeVisitorStub() { @Override public TraversalControl visitGraphQLTypeReference(GraphQLTypeReference typeRef, TraverserContext context) { GraphQLNamedType graphQLNamedType = changedTypes.get(typeRef.getName()); if (graphQLNamedType != null) { typeRef = GraphQLTypeReference.typeRef(graphQLNamedType.getName()); return changeNode(context, typeRef); } return CONTINUE; } }; traverseAndTransform(dummyRoot, new HashMap<>(), new HashMap<>(), typeRefVisitor, codeRegistry, schema); } private boolean traverseAndTransform(DummyRoot dummyRoot, Map changedTypes, Map typeReferences, GraphQLTypeVisitor visitor, GraphQLCodeRegistry.Builder codeRegistry, GraphQLSchema schema) { List> zippers = new LinkedList<>(); Map> zipperByNodeAfterTraversing = new LinkedHashMap<>(); Map> zipperByOriginalNode = new LinkedHashMap<>(); Map, List>>> breadcrumbsByZipper = new LinkedHashMap<>(); Map> reverseDependencies = new LinkedHashMap<>(); Map> typeRefReverseDependencies = new LinkedHashMap<>(); TraverserVisitor nodeTraverserVisitor = new TraverserVisitor<>() { @Override public TraversalControl enter(TraverserContext context) { GraphQLSchemaElement currentSchemaElement = context.thisNode(); if (currentSchemaElement == dummyRoot) { return TraversalControl.CONTINUE; } if (currentSchemaElement instanceof GraphQLTypeReference) { GraphQLTypeReference typeRef = (GraphQLTypeReference) currentSchemaElement; typeReferences.put(typeRef.getName(), typeRef); } NodeZipper nodeZipper = new NodeZipper<>(currentSchemaElement, context.getBreadcrumbs(), SCHEMA_ELEMENT_ADAPTER); context.setVar(NodeZipper.class, nodeZipper); context.setVar(NodeAdapter.class, SCHEMA_ELEMENT_ADAPTER); int zippersBefore = zippers.size(); TraversalControl result = currentSchemaElement.accept(context, visitor); // detection if the node was changed if (zippersBefore + 1 == zippers.size()) { nodeZipper = zippers.get(zippers.size() - 1); if (context.originalThisNode() instanceof GraphQLNamedType && context.isChanged()) { GraphQLNamedType originalNamedType = (GraphQLNamedType) context.originalThisNode(); GraphQLNamedType changedNamedType = (GraphQLNamedType) context.thisNode(); if (!originalNamedType.getName().equals(changedNamedType.getName())) { changedTypes.put(originalNamedType.getName(), changedNamedType); } } } 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()); if (context.originalThisNode() instanceof GraphQLTypeReference) { String typeName = ((GraphQLTypeReference) context.originalThisNode()).getName(); typeRefReverseDependencies.computeIfAbsent(typeName, 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()); if (zipper.getModificationType() == DELETE) { return CONTINUE; } 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); if (codeRegistry != null) { traverser.rootVar(GraphQLCodeRegistry.Builder.class, codeRegistry); } if (schema != null) { traverser.rootVar(GraphQLSchema.class, schema); } traverser.traverse(dummyRoot, nodeTraverserVisitor); List> stronglyConnectedTopologicallySorted = getStronglyConnectedComponentsTopologicallySorted(reverseDependencies, typeRefReverseDependencies); return zipUpToDummyRoot(zippers, stronglyConnectedTopologicallySorted, breadcrumbsByZipper, zipperByNodeAfterTraversing); } private static class RelevantZippersAndBreadcrumbs { final Multimap> zipperByParent = LinkedHashMultimap.create(); final Set> relevantZippers; final Map, List>>> breadcrumbsByZipper; public RelevantZippersAndBreadcrumbs(List> relevantZippers, Map, List>>> breadcrumbsByZipper) { this.relevantZippers = new LinkedHashSet<>(relevantZippers); this.breadcrumbsByZipper = breadcrumbsByZipper; for (NodeZipper zipper : relevantZippers) { for (List> breadcrumbs : breadcrumbsByZipper.get(zipper)) { zipperByParent.put(breadcrumbs.get(0).getNode(), zipper); } } } public boolean isRelevantZipper(NodeZipper zipper) { return relevantZippers.contains(zipper); } public Collection> zippersWithParent(GraphQLSchemaElement parent) { return zipperByParent.get(parent); } public void removeRelevantZipper(NodeZipper zipper) { relevantZippers.remove(zipper); } public List>> getBreadcrumbs(NodeZipper zipper) { return breadcrumbsByZipper.get(zipper); } public void updateZipper(NodeZipper currentZipper, NodeZipper newZipper) { // the current zipper is not always relevant, meaning this has no effect sometimes relevantZippers.remove(currentZipper); relevantZippers.add(newZipper); List>> currentBreadcrumbs = breadcrumbsByZipper.get(currentZipper); assertNotNull(currentBreadcrumbs, "No breadcrumbs found for zipper %s", currentZipper); for (List> breadcrumbs : currentBreadcrumbs) { GraphQLSchemaElement parent = breadcrumbs.get(0).getNode(); zipperByParent.remove(parent, currentZipper); zipperByParent.put(parent, newZipper); } breadcrumbsByZipper.remove(currentZipper); breadcrumbsByZipper.put(newZipper, currentBreadcrumbs); } } private boolean zipUpToDummyRoot(List> zippers, List> stronglyConnectedTopologicallySorted, Map, List>>> breadcrumbsByZipper, Map> nodeToZipper) { if (zippers.size() == 0) { return false; } RelevantZippersAndBreadcrumbs relevantZippers = new RelevantZippersAndBreadcrumbs(zippers, breadcrumbsByZipper); for (int i = stronglyConnectedTopologicallySorted.size() - 1; i >= 0; i--) { List scc = stronglyConnectedTopologicallySorted.get(i); // performance relevant: we avoid calling zipperWithSameParent twice // for SCC of size one. if (scc.size() > 1) { boolean sccChanged = false; List unchangedSccElements = new ArrayList<>(); for (GraphQLSchemaElement element : scc) { // if the current element itself has a zipper it is changed if (relevantZippers.isRelevantZipper(nodeToZipper.get(element))) { sccChanged = true; continue; } // if the current element is changed via "moveUp" it is changed Map, Breadcrumb> zipperWithSameParent = zipperWithSameParent(element, relevantZippers, false); if (zipperWithSameParent.size() > 0) { sccChanged = true; } else { unchangedSccElements.add(element); } } if (!sccChanged) { continue; } // we need to change all elements inside the current SCC for (GraphQLSchemaElement element : unchangedSccElements) { NodeZipper currentZipper = nodeToZipper.get(element); NodeZipper newZipper = currentZipper.withNewNode(element.copy()); nodeToZipper.put(element, newZipper); relevantZippers.updateZipper(currentZipper, newZipper); } } for (int j = scc.size() - 1; j >= 0; j--) { GraphQLSchemaElement element = scc.get(j); Map, Breadcrumb> zipperWithSameParent = zipperWithSameParent(element, relevantZippers, true); // 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; } NodeZipper curZipperForElement = nodeToZipper.get(element); assertNotNull(curZipperForElement, "curZipperForElement is null for parentNode %s", element); relevantZippers.updateZipper(curZipperForElement, newZipper); } } return true; } private Map, Breadcrumb> zipperWithSameParent( GraphQLSchemaElement parent, RelevantZippersAndBreadcrumbs relevantZippers, boolean cleanup) { Map, Breadcrumb> result = new LinkedHashMap<>(); Collection> zippersWithParent = relevantZippers.zippersWithParent(parent); Iterator> zippersIter = zippersWithParent.iterator(); outer: while (zippersIter.hasNext()) { NodeZipper zipper = zippersIter.next(); List>> listOfBreadcrumbsList = assertNotNull(relevantZippers.getBreadcrumbs(zipper)); for (int i = 0; i < listOfBreadcrumbsList.size(); i++) { List> path = listOfBreadcrumbsList.get(i); if (path.get(0).getNode() == parent) { result.put(zipper, path.get(0)); if (cleanup) { // remove breadcrumb we just used listOfBreadcrumbsList.remove(i); if (listOfBreadcrumbsList.size() == 0) { // if there are no breadcrumbs left for this zipper it is safe to remove relevantZippers.removeRelevantZipper(zipper); } } continue outer; } } } return result; } private NodeZipper moveUp( GraphQLSchemaElement parent, Map, Breadcrumb> 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) { Breadcrumb breadcrumb = sameParentsZipper.get(zipper); zipperWithOneParents.add(new ZipperWithOneParent(zipper, breadcrumb)); } 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 = ImmutableKit.emptyList(); } return new NodeZipper<>(newNode, newBreadcrumbs, SCHEMA_ELEMENT_ADAPTER); } private static class ZipperWithOneParent { public NodeZipper zipper; public Breadcrumb parent; public ZipperWithOneParent(NodeZipper zipper, Breadcrumb parent) { this.zipper = zipper; this.parent = parent; } } // 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 SCHEMA_APPLIED_DIRECTIVES = "schemaAppliedDirectives"; static final String INTROSPECTION = "introspection"; static final String SCHEMA_ELEMENT = "schemaElement"; GraphQLSchema schema; GraphQLObjectType query; GraphQLObjectType mutation; GraphQLObjectType subscription; GraphQLObjectType introspectionSchemaType; Set additionalTypes; Set directives; Set schemaDirectives; Set schemaAppliedDirectives; GraphQLSchemaElement schemaElement; 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()); schemaAppliedDirectives = new LinkedHashSet<>(schema.getSchemaAppliedDirectives()); directives = new LinkedHashSet<>(schema.getDirectives()); introspectionSchemaType = schema.getIntrospectionSchemaType(); } DummyRoot(GraphQLSchemaElement schemaElement) { this.schemaElement = schemaElement; } @Override public GraphQLSchemaElement copy() { return assertShouldNeverHappen(); } @Override public List getChildren() { return assertShouldNeverHappen(); } @Override public SchemaElementChildrenContainer getChildrenWithTypeReferences() { SchemaElementChildrenContainer.Builder builder = newSchemaElementChildrenContainer(); if (schemaElement != null) { builder.child(SCHEMA_ELEMENT, schemaElement); } else { builder.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.children(SCHEMA_APPLIED_DIRECTIVES, schemaAppliedDirectives); builder.child(INTROSPECTION, introspectionSchemaType); } return builder.build(); } @Override public GraphQLSchemaElement withNewChildren(SchemaElementChildrenContainer newChildren) { if (this.schemaElement != null) { this.schemaElement = newChildren.getChildOrNull(SCHEMA_ELEMENT); return this; } // 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); introspectionSchemaType = newChildren.getChildOrNull(INTROSPECTION); additionalTypes = new LinkedHashSet<>(newChildren.getChildren(ADD_TYPES)); directives = new LinkedHashSet<>(newChildren.getChildren(DIRECTIVES)); schemaDirectives = new LinkedHashSet<>(newChildren.getChildren(SCHEMA_DIRECTIVES)); schemaAppliedDirectives = new LinkedHashSet<>(newChildren.getChildren(SCHEMA_APPLIED_DIRECTIVES)); return this; } @Override public TraversalControl accept(TraverserContext context, GraphQLTypeVisitor visitor) { return assertShouldNeverHappen(); } public GraphQLSchema rebuildSchema(GraphQLCodeRegistry.Builder codeRegistry) { return GraphQLSchema.newSchema() .query(this.query) .mutation(this.mutation) .subscription(this.subscription) .additionalTypes(this.additionalTypes) .additionalDirectives(this.directives) .introspectionSchemaType(this.introspectionSchemaType) .withSchemaDirectives(this.schemaDirectives) .withSchemaAppliedDirectives(this.schemaAppliedDirectives) .codeRegistry(codeRegistry.build()) .description(schema.getDescription()) .build(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy