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

io.apicurio.datamodels.openapi.visitors.dereference.Dereferencer Maven / Gradle / Ivy

package io.apicurio.datamodels.openapi.visitors.dereference;

import io.apicurio.datamodels.Library;
import io.apicurio.datamodels.core.models.Document;
import io.apicurio.datamodels.core.models.IReferenceNode;
import io.apicurio.datamodels.core.models.Node;
import io.apicurio.datamodels.core.util.IReferenceResolver;
import io.apicurio.datamodels.core.util.ReferenceResolverChain;
import io.apicurio.datamodels.core.util.VisitorUtil;
import io.apicurio.datamodels.core.visitors.TraverserDirection;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
 * Given a {@link Document}, process its reference strings and attempt to dereference them
 * using {@link io.apicurio.datamodels.core.util.IReferenceResolver}. Produce a clone of the original model,
 * with the resolved components/definitions attached.
 *
 * @author Jakub Senko 
 */
public class Dereferencer {

    private final Document source;
    private final IReferenceResolver resolver;
    private final boolean strict;

    private final Set unresolvable = new HashSet<>();

    /**
     * Setup the dereferencer execution with default config.
     *
     * @param source original {@link Document}
     */
    public Dereferencer(Document source) {
        this(source, ReferenceResolverChain.getInstance(), false);
    }

    /**
     * Setup the dereferencer execution.
     *
     * @param source   original {@link Document}
     * @param resolver provide a custom resolver, otherwise the common {@link io.apicurio.datamodels.core.util.ReferenceResolverChain}
     *                 is used.
     * @param strict   If true, and there are any references that cannot be resolved, raise an exception.
     *                 Defaults to false.
     */
    public Dereferencer(Document source, IReferenceResolver resolver, boolean strict) {
        this.source = source;
        this.resolver = resolver;
        this.strict = strict;
    }

    /**
     * Execute the algorithm.
     *
     * @return dereferenced clone of the source document.
     * @throws java.lang.RuntimeException if strict and some references could not be dereferenced
     */
    public Document dereference() {
        Document clone = Library.cloneDocument(source);

        IReferenceManipulationStrategy strategy = null;
        switch (clone.getDocumentType()) {
            case asyncapi2:
                strategy = new Aai20IReferenceManipulationStrategy();
                break;
            case openapi2:
                strategy = new Oas20IReferenceManipulationStrategy();
                break;
            case openapi3:
                strategy = new Oas30IReferenceManipulationStrategy();
                break;
            default:
                throw new RuntimeException("Unknown document type: " + clone.getDocumentType());
        }

        // Keeps the nodes waiting to be processed (BFS-style)
        Queue processQueue = new LinkedList<>();
        // start with the whole model
        processQueue.add(new Context(null, clone));

        // Prevents recursive loops
        Map resolvedToLocalMap = new HashMap<>();
        for (String ref : strategy.getExistingLocalComponents(clone).keySet()) {
            resolvedToLocalMap.put(ref, ref);
        }

        while (!processQueue.isEmpty()) {
            Context item = processQueue.remove();

            // Local components
            Map localComponents = strategy.getExistingLocalComponents(clone);

            // Collect all reference objects in the processed node
            ReferenceCollectionVisitor rcv = new ReferenceCollectionVisitor();
            VisitorUtil.visitTree(item.node, rcv, TraverserDirection.down);
            List referencedNodes = rcv.getReferencedNodes();

            // For each reference node ...
            for (IReferenceNode node : referencedNodes) {

                // skip if already local, this makes sense if we're initially processing the whole Document
                if (item.parentRef == null && localComponents.containsKey(node.getReference()))
                    continue;

                // Reference to be resolved
                Reference originalRef = new Reference(node.getReference());
                Reference resolvedRef = originalRef;

                // Attempt to resolve
                if(item.parentRef != null && resolvedRef.isRelative()) {
                    resolvedRef = resolvedRef.withAbsoluteFrom(new Reference(item.parentRef));
                }

                // if we've already seen the resolved reference, just point to the existing one
                // to avoid cycles
                if (resolvedToLocalMap.containsKey(resolvedRef.getRef())) {
                    node.setReference(resolvedToLocalMap.get(resolvedRef.getRef()));
                    continue;
                }

                Node resolved = resolver.resolveRef(resolvedRef.getRef(), (Node) node);

                // if null keep the reference in an 'unresolvable' set to decide later
                if (resolved == null) {
                    unresolvable.add(node.getReference());
                } else {
                    IReferenceManipulationStrategy.ReferenceAndNode localRef = null;

                    // try to attach
                    // repeat in case of name conflict
                    String name = originalRef.getName();
                    for (int i = 0; i < Integer.MAX_VALUE; i++) {
                        if (i > 0)
                            name = originalRef.getName() + i;
                        try {
                            localRef = strategy.attachAsDefinition(clone, name, resolved);
                            break;
                        } catch (IllegalArgumentException ex) {
                            // try a different name
                            // TODO maybe avoid exceptions?
                        }
                    }

                    if (localRef == null) {
                        unresolvable.add(node.getReference());
                    } else {
                        // success!
                        // rename the original reference
                        node.setReference(localRef.getRef());
                        // add resolved node to processing queue
                        processQueue.add(new Context(originalRef.getRef(), localRef.getNode()));
                        // remember, to prevent cycles
                        resolvedToLocalMap.put(resolvedRef.getRef(), localRef.getRef());
                    }
                }
            }
        }
        // we're done, if strict, throw
        if (unresolvable.size() != 0 && strict)
            throw new RuntimeException("Could not resolve some references: " + unresolvable);

        return clone;
    }

    public IReferenceResolver getResolver() {
        return resolver;
    }

    public boolean isStrict() {
        return strict;
    }

    /**
     * After {@link #dereference()} is executed and strict is false,
     *
     * @return a possibly non-empty set of unresolved references
     */
    public Set getUnresolvableReferences() {
        return new HashSet<>(unresolvable);
    }

    /**
     * State for each recursive step
     */
    private class Context { // TODO maybe merge with IReferenceManipulationStrategy.ReferenceAndNode}

        private String parentRef;
        private Node node;

        private Context(String parentRef, Node node) {
            this.parentRef = parentRef;
            this.node = node;
        }

        @Override
        public boolean equals(Object o) { // java.util.Objects are not supported by JSweet
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Context context = (Context) o;

            if (parentRef != null ? !parentRef.equals(context.parentRef) : context.parentRef != null)
                return false;
            return node != null ? node.equals(context.node) : context.node == null;
        }

        @Override
        public int hashCode() {
            int result = parentRef != null ? parentRef.hashCode() : 0;
            result = 31 * result + (node != null ? node.hashCode() : 0);
            return result;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy