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 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