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

org.opendaylight.restconf.server.spi.NormalizedNodeWriter Maven / Gradle / Ivy

There is a newer version: 8.0.3
Show newest version
/*
 * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */
package org.opendaylight.restconf.server.spi;

import static java.util.Objects.requireNonNull;

import com.google.common.annotations.Beta;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.query.DepthParam;
import org.opendaylight.restconf.api.query.FieldsParam;
import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
import org.opendaylight.restconf.server.api.ServerException;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;

/**
 * This is an experimental iterator over a {@link NormalizedNode}. This is essentially
 * the opposite of a {@link javax.xml.stream.XMLStreamReader} -- unlike instantiating an iterator over
 * the backing data, this encapsulates a {@link NormalizedNodeStreamWriter} and allows
 * us to write multiple nodes.
 */
@NonNullByDefault
public abstract class NormalizedNodeWriter implements Flushable, Closeable {
    protected final NormalizedNodeStreamWriter writer;

    NormalizedNodeWriter(final NormalizedNodeStreamWriter writer) {
        this.writer = requireNonNull(writer);
    }

    /**
     * Create a new writer backed by a {@link NormalizedNodeStreamWriter}.
     *
     * @param writer Back-end writer
     * @param maxDepth Maximal depth to write
     * @return A new instance.
     */
    public static final NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
            final @Nullable DepthParam maxDepth) {
        return forStreamWriter(writer, true,  maxDepth, null);
    }

    /**
     * Create a new writer backed by a {@link NormalizedNodeStreamWriter}.
     *
     * @param writer Back-end writer
     * @param maxDepth Maximal depth to write
     * @param fields Selected child nodes to write
     * @return A new instance.
     */
    public static final NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
            final @Nullable DepthParam maxDepth, final @Nullable List> fields) {
        return forStreamWriter(writer, true,  maxDepth, fields);
    }

    /**
     * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. Unlike the simple
     * {@link #forStreamWriter(NormalizedNodeStreamWriter, DepthParam, List)} method, this allows the caller to
     * switch off RFC6020 XML compliance, providing better throughput. The reason is that the XML mapping rules in
     * RFC6020 require the encoding to emit leaf nodes which participate in a list's key first and in the order in which
     * they are defined in the key. For JSON, this requirement is completely relaxed and leaves can be ordered in any
     * way we see fit. The former requires a bit of work: first a lookup for each key and then for each emitted node we
     * need to check whether it was already emitted.
     *
     * @param writer Back-end writer
     * @param orderKeyLeaves whether the returned instance should be RFC6020 XML compliant.
     * @param depth Maximal depth to write
     * @param fields Selected child nodes to write
     * @return A new instance.
     */
    public static final NormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
            final boolean orderKeyLeaves, final @Nullable DepthParam depth, final @Nullable List> fields) {
        return new DefaultNormalizedNodeWriter(writer, !orderKeyLeaves, depth, fields);
    }

    /**
     * Translate a {@link FieldsParam} to a complete list of child nodes organized into levels, suitable for use with
     * {@link NormalizedNodeWriter}.
     *
     * 

* Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output it is only * possible to assume on what depth the selected element is placed. Identifiers of intermediary mixin nodes are also * flatten to the same level as identifiers of data nodes.
* Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
*

     * level 0: ['a', 'd']
     * level 1: ['b', 'x', 'e']
     * level 2: ['c']
     * 
* * @param modelContext EffectiveModelContext * @param startNode {@link DataSchemaContext} of the API request path * @param input input value of fields parameter * @return {@link List} of levels; each level contains set of {@link QName} */ @Beta public static List> translateFieldsParam(final EffectiveModelContext modelContext, final DataSchemaContext startNode, final FieldsParam input) throws ServerException { final var parsed = new ArrayList>(); processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(), startNode, input.nodeSelectors(), 0); return parsed; } private static void processSelectors(final List> parsed, final EffectiveModelContext context, final QNameModule startNamespace, final DataSchemaContext startNode, final List selectors, final int index) throws ServerException { final Set startLevel; if (parsed.size() <= index) { startLevel = new HashSet<>(); parsed.add(startLevel); } else { startLevel = parsed.get(index); } for (var selector : selectors) { var node = startNode; var namespace = startNamespace; var level = startLevel; var levelIndex = index; // Note: path is guaranteed to have at least one step final var it = selector.path().iterator(); while (true) { // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We // need to call it only when we know there is another identifier coming, otherwise we would end // up with empty levels sneaking into the mix. // // Dealing with that weirdness requires understanding what the expected end results are and a // larger rewrite of the algorithms involved. final var step = it.next(); final var module = step.module(); if (module != null) { // FIXME: this is not defensive enough, as we can fail to find the module namespace = context.findModules(module).iterator().next().getQNameModule(); } // add parsed identifier to results for current level node = addChildToResult(node, step.identifier().bindTo(namespace), level); if (!it.hasNext()) { break; } // go one level down level = prepareQNameLevel(parsed, level); levelIndex++; } final var subs = selector.subSelectors(); if (!subs.isEmpty()) { processSelectors(parsed, context, namespace, node, subs, levelIndex + 1); } } } /** * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers * is allocated and pushed to input parsed identifiers. * * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels. * @param currentLevel Current level of identifiers (set). * @return Existing or new level of identifiers. */ private static Set prepareQNameLevel(final List> parsedIdentifiers, final Set currentLevel) { final var existingLevel = parsedIdentifiers.stream() .filter(qNameSet -> qNameSet.equals(currentLevel)) .findAny(); if (existingLevel.isPresent()) { final int index = parsedIdentifiers.indexOf(existingLevel.orElseThrow()); if (index == parsedIdentifiers.size() - 1) { final var nextLevel = new HashSet(); parsedIdentifiers.add(nextLevel); return nextLevel; } return parsedIdentifiers.get(index + 1); } final var nextLevel = new HashSet(); parsedIdentifiers.add(nextLevel); return nextLevel; } /** * Add parsed child of current node to result for current level. * * @param currentNode current node * @param childQName parsed identifier of child node * @param level current nodes level * @return {@link DataSchemaContextNode} */ private static DataSchemaContext addChildToResult(final DataSchemaContext currentNode, final QName childQName, final Set level) throws ServerException { // resolve parent node final var parentNode = resolveMixinNode(currentNode, level, currentNode.dataSchemaNode().getQName()); if (parentNode == null) { throw new ServerException(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, "Not-mixin node missing in %s", currentNode.getPathStep().getNodeType().getLocalName()); } // resolve child node final var childNode = resolveMixinNode(childByQName(parentNode, childQName), level, childQName); if (childNode == null) { throw new ServerException(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, "Child %s node missing in %s", childQName.getLocalName(), currentNode.getPathStep().getNodeType().getLocalName()); } // add final childNode node to level nodes level.add(childNode.dataSchemaNode().getQName()); return childNode; } private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) { return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null; } /** * Resolve mixin node by searching for inner nodes until not mixin node or null is found. * All nodes expect of not mixin node are added to current level nodes. * * @param node initial mixin or not-mixin node * @param level current nodes level * @param qualifiedName qname of initial node * @return {@link DataSchemaContextNode} */ private static @Nullable DataSchemaContext resolveMixinNode(final @Nullable DataSchemaContext node, final @NonNull Set level, final @NonNull QName qualifiedName) { DataSchemaContext currentNode = node; while (currentNode instanceof PathMixin currentMixin) { level.add(qualifiedName); currentNode = currentMixin.childByQName(qualifiedName); } return currentNode; } @Override public final void flush() throws IOException { writer.flush(); } @Override public final void close() throws IOException { writer.flush(); writer.close(); } /** * Iterate over the provided {@link NormalizedNode} and emit write events to the encapsulated * {@link NormalizedNodeStreamWriter}. * * @param node Node * @return {@code ParameterAwareNormalizedNodeWriter} * @throws IOException when thrown from the backing writer. */ public final NormalizedNodeWriter write(final NormalizedNode node) throws IOException { if (node instanceof ContainerNode n) { writeContainer(n); } else if (node instanceof MapNode n) { writeMap(n); } else if (node instanceof MapEntryNode n) { writeMapEntry(n); } else if (node instanceof LeafNode n) { writeLeaf(n); } else if (node instanceof ChoiceNode n) { writeChoice(n); } else if (node instanceof UnkeyedListNode n) { writeUnkeyedList(n); } else if (node instanceof UnkeyedListEntryNode n) { writeUnkeyedListEntry(n); } else if (node instanceof LeafSetNode n) { writeLeafSet(n); } else if (node instanceof LeafSetEntryNode n) { writeLeafSetEntry(n); } else if (node instanceof AnydataNode n) { writeAnydata(n); } else if (node instanceof AnyxmlNode n) { writeAnyxml(n); } else { throw new IOException("Unhandled contract " + node.contract().getSimpleName()); } return this; } protected abstract void writeAnydata(AnydataNode node) throws IOException; protected abstract void writeAnyxml(AnyxmlNode node) throws IOException; protected abstract void writeChoice(ChoiceNode node) throws IOException; protected abstract void writeContainer(ContainerNode node) throws IOException; protected abstract void writeLeaf(LeafNode node) throws IOException; protected abstract void writeLeafSet(LeafSetNode node) throws IOException; protected abstract void writeLeafSetEntry(LeafSetEntryNode node) throws IOException; protected abstract void writeMap(MapNode node) throws IOException; protected abstract void writeMapEntry(MapEntryNode node) throws IOException; protected abstract void writeUnkeyedList(UnkeyedListNode node) throws IOException; protected abstract void writeUnkeyedListEntry(UnkeyedListEntryNode node) throws IOException; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy