From db38dd17189880dc001e4c174f2040a862a030c5 Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Thu, 28 Feb 2019 13:48:56 +0100 Subject: [PATCH] Add RFC7952 data model extensions RFC7952 has implications on NormalizedNodeStreamWriter, namely the ability to emit metadata attached to NormalizedNodes. This patch introduces the concept of NormalizedMetadata, which is the metadata counterpart to NormalizedNode. It also defines NormalizedMetadataStreamWriter as an extension of NormalizedNodeStreamWriter and implements NormalizedMetadataWriter, which piggy-backs a NormalizedMetadata structure on top of a NormalizedNode structure, so that the two are emitted as a single event stream. JIRA: YANGTOOLS-961 Change-Id: I8f1a69361d18cf5f9c14185778cca7c2d6ebc4f7 Signed-off-by: Robert Varga --- artifacts/pom.xml | 10 + features/odl-yangtools-data-api/pom.xml | 8 + yang/pom.xml | 2 + yang/rfc7952-data-api/pom.xml | 62 +++++++ .../rfc7952/data/api/NormalizedMetadata.java | 43 +++++ .../data/api/NormalizedMetadataContainer.java | 29 +++ .../api/NormalizedMetadataStreamWriter.java | 78 ++++++++ yang/rfc7952-data-util/pom.xml | 66 +++++++ .../data/util/NormalizedMetadataWriter.java | 127 +++++++++++++ ...izedNodeStreamWriterMetadataDecorator.java | 172 ++++++++++++++++++ 10 files changed, 597 insertions(+) create mode 100644 yang/rfc7952-data-api/pom.xml create mode 100644 yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadata.java create mode 100644 yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataContainer.java create mode 100644 yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataStreamWriter.java create mode 100644 yang/rfc7952-data-util/pom.xml create mode 100644 yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedMetadataWriter.java create mode 100644 yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedNodeStreamWriterMetadataDecorator.java diff --git a/artifacts/pom.xml b/artifacts/pom.xml index 8f166773b4..d7a73660cb 100644 --- a/artifacts/pom.xml +++ b/artifacts/pom.xml @@ -164,6 +164,16 @@ 3.0.0-SNAPSHOT + + org.opendaylight.yangtools + rfc7952-data-api + 3.0.0-SNAPSHOT + + + org.opendaylight.yangtools + rfc7952-data-util + 3.0.0-SNAPSHOT + org.opendaylight.yangtools rfc7952-model-api diff --git a/features/odl-yangtools-data-api/pom.xml b/features/odl-yangtools-data-api/pom.xml index 0c769efcb6..cf64d8fb06 100644 --- a/features/odl-yangtools-data-api/pom.xml +++ b/features/odl-yangtools-data-api/pom.xml @@ -50,5 +50,13 @@ org.opendaylight.yangtools yang-data-util + + org.opendaylight.yangtools + rfc7952-data-api + + + org.opendaylight.yangtools + rfc7952-data-util + diff --git a/yang/pom.xml b/yang/pom.xml index 73c9914334..aab8e175b2 100644 --- a/yang/pom.xml +++ b/yang/pom.xml @@ -74,6 +74,8 @@ rfc6536-parser-support + rfc7952-data-api + rfc7952-data-util rfc7952-model-api rfc7952-parser-support diff --git a/yang/rfc7952-data-api/pom.xml b/yang/rfc7952-data-api/pom.xml new file mode 100644 index 0000000000..812c2a81a5 --- /dev/null +++ b/yang/rfc7952-data-api/pom.xml @@ -0,0 +1,62 @@ + + + + + 4.0.0 + + org.opendaylight.yangtools + bundle-parent + 3.0.0-SNAPSHOT + ../../bundle-parent + + + rfc7952-data-api + bundle + ${project.artifactId} + RFC7952 data model extensions + + + + org.opendaylight.yangtools + concepts + + + org.opendaylight.yangtools + yang-common + + + org.opendaylight.yangtools + yang-data-api + + + org.opendaylight.yangtools + rfc7952-model-api + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + checkstyle.violationSeverity=error + + + + com.github.spotbugs + spotbugs-maven-plugin + + true + + + + + + diff --git a/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadata.java b/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadata.java new file mode 100644 index 0000000000..813d7ffc02 --- /dev/null +++ b/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadata.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 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.yangtools.rfc7952.data.api; + +import com.google.common.annotations.Beta; +import java.util.Map; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.yangtools.concepts.Identifiable; +import org.opendaylight.yangtools.concepts.Immutable; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; +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.UnkeyedListNode; + +/** + * RFC7952 metadata counterpart to a {@link NormalizedNode}. This interface is meant to be used as a companion to + * a NormalizedNode instance, hence it does not support iterating over its structure like it is possible with + * {@link NormalizedNode#getValue()}. + * + *

+ * This model of metadata does not have the RFC7952 restriction on metadata attachment to {@code list}s and + * {@code leaf-list}s because NormalizedNode data model has {@link LeafSetNode}, {@link MapNode} and + * {@link UnkeyedListNode} to which metadata can be attached. + * + * @author Robert Varga + */ +@Beta +public interface NormalizedMetadata extends Identifiable, Immutable { + /** + * Return the set of annotations defined in this metadata node. Values are expected to be effectively-immutable + * scalar types, like {@link String}s, {@link Number}s and similar. + * + * @return The set of annotations attached to the corresponding data node. + */ + @NonNull Map getAnnotations(); +} diff --git a/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataContainer.java b/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataContainer.java new file mode 100644 index 0000000000..99b939eb4d --- /dev/null +++ b/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataContainer.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 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.yangtools.rfc7952.data.api; + +import com.google.common.annotations.Beta; +import java.util.Optional; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer; + +/** + * RFC7952 metadata counterpart to a {@link NormalizedNodeContainer}. + * + * @author Robert Varga + */ +@Beta +public interface NormalizedMetadataContainer extends NormalizedMetadata { + /** + * Returns child node identified by provided key. + * + * @param child Path argument identifying child node + * @return Optional with child node if child exists, {@link Optional#empty()} if it does not. + */ + Optional getChild(PathArgument child); +} diff --git a/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataStreamWriter.java b/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataStreamWriter.java new file mode 100644 index 0000000000..f4b9e3477b --- /dev/null +++ b/yang/rfc7952-data-api/src/main/java/org/opendaylight/yangtools/rfc7952/data/api/NormalizedMetadataStreamWriter.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019 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.yangtools.rfc7952.data.api; + +import com.google.common.annotations.Beta; +import java.io.IOException; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.yangtools.rfc7952.model.api.AnnotationSchemaNode; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriterExtension; + +/** + * Extension to the NormalizedNodeStreamWriter with metadata support. Semantically this extends the event model of + * {@link NormalizedNodeStreamWriter} with two new events: + *

+ * + *

+ * Note that some implementations of this interface, notably those targeting streaming XML, may require metadata to + * be emitted before any other events. Such requirement is communicated through {@link #requireMetadataFirst()} and + * users must honor it. If such requirement is not set, metadata may be emitted at any time. + * + *

+ * Furthermore implementations targeting RFC7952 encoding towards external systems are required to handle metadata + * attached to {@code leaf-list} and {@code list} nodes by correctly extending them to each entry. + */ +@Beta +public interface NormalizedMetadataStreamWriter extends NormalizedNodeStreamWriterExtension { + /** + * Start the metadata block for the currently-open node. Allowed events are + * {@link #startMetadataEntry(QName, AnnotationSchemaNode)} and {@link NormalizedNodeStreamWriter#endNode()}. + * + * @param childSizeHint Non-negative count of expected direct child nodes or + * {@link NormalizedNodeStreamWriter#UNKNOWN_SIZE} if count is unknown. This is only hint and + * should not fail writing of child events, if there are more events than count. + * @throws IllegalStateException if current node already has a metadata block or cannot receive metadata -- for + * example because {@link #requireMetadataFirst()} was not honored. + * @throws IOException if an underlying IO error occurs + */ + void startMetadata(int childSizeHint) throws IOException; + + /** + * Start a new metadata entry. The value of the metadata entry should be emitted through + * {@link NormalizedNodeStreamWriter#nodeValue(Object)}. + * + * @param name Metadata name, as defined through {@code md:annotation} + * @param schema Effective {@code md:annotation} schema, or null if unknown to the caller + * @throws NullPointerException if {@code name} is null + * @throws IllegalStateException when this method is invoked outside of a metadata block + * @throws IOException if an underlying IO error occurs + */ + void startMetadataEntry(@NonNull QName name, @Nullable AnnotationSchemaNode schema) throws IOException; + + /** + * Indicate whether metadata is required to be emitted just after an entry is open. The default implementation + * returns false. + * + * @return True if metadata must occur just after the start of an entry. + */ + default boolean requireMetadataFirst() { + return false; + } +} diff --git a/yang/rfc7952-data-util/pom.xml b/yang/rfc7952-data-util/pom.xml new file mode 100644 index 0000000000..0449171afb --- /dev/null +++ b/yang/rfc7952-data-util/pom.xml @@ -0,0 +1,66 @@ + + + + + 4.0.0 + + org.opendaylight.yangtools + bundle-parent + 3.0.0-SNAPSHOT + ../../bundle-parent + + + rfc7952-data-util + bundle + ${project.artifactId} + RFC7952 data model utilities + + + + org.opendaylight.yangtools + concepts + + + org.opendaylight.yangtools + yang-common + + + org.opendaylight.yangtools + yang-data-api + + + org.opendaylight.yangtools + rfc7952-data-api + + + org.opendaylight.yangtools + rfc7952-model-api + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + checkstyle.violationSeverity=error + + + + com.github.spotbugs + spotbugs-maven-plugin + + true + + + + + + diff --git a/yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedMetadataWriter.java b/yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedMetadataWriter.java new file mode 100644 index 0000000000..aac60f8c80 --- /dev/null +++ b/yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedMetadataWriter.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019 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.yangtools.rfc7952.data.util; + +import static com.google.common.base.Preconditions.checkArgument; +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 javax.annotation.concurrent.NotThreadSafe; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata; +import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadataStreamWriter; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; + +/** + * A utility class to attach {@link NormalizedMetadata} into a NormalizedNode stream, such as the one produced by + * {@link NormalizedNodeWriter}, so that a target {@link NormalizedNodeStreamWriter} sees both data and metadata in + * the stream. A typical use would like this: + * + *

+ * + * // Data for output + * NormalizedNode<?, ?> data; + * // Metadata for output + * NormalizedMetadata metadata; + * + * // Target output writer + * NormalizedNodeStreamWriter output = ...; + * // Metadata writer + * NormalizedMetadataStreamWriter metaWriter = NormalizedMetadataWriter.forStreamWriter(output); + * + * // Write a normalized node and its metadata + * dataWriter.write(data, metadata); + * + * + * @author Robert Varga + */ +@Beta +@NotThreadSafe +public final class NormalizedMetadataWriter implements Closeable, Flushable { + private final NormalizedNodeStreamWriter writer; + private final boolean orderKeyLeaves; + + private NormalizedMetadataWriter(final NormalizedNodeStreamWriter writer, final boolean orderKeyLeaves) { + this.writer = requireNonNull(writer); + this.orderKeyLeaves = orderKeyLeaves; + } + + /** + * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. Unlike the simple + * {@link #forStreamWriter(NormalizedNodeStreamWriter)} 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. + * @return A new instance. + */ + public static @NonNull NormalizedMetadataWriter forStreamWriter(final NormalizedNodeStreamWriter writer, + final boolean orderKeyLeaves) { + return new NormalizedMetadataWriter(writer, orderKeyLeaves); + } + + /** + * Create a new writer backed by a {@link NormalizedNodeStreamWriter}. This is a convenience method for + * {@code forStreamWriter(writer, true)}. + * + * @param writer Back-end writer + * @return A new instance. + */ + public static @NonNull NormalizedMetadataWriter forStreamWriter(final NormalizedNodeStreamWriter writer) { + return forStreamWriter(writer, true); + } + + /** + * Iterate over the provided {@link NormalizedNode} and {@link NormalizedMetadata} and emit write events to the + * encapsulated {@link NormalizedNodeStreamWriter}. + * + * @param data NormalizedNode data + * @param metadata {@link NormalizedMetadata} metadata + * @return NormalizedNodeWriter this + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if metadata does not match data + * @throws IOException when thrown from the backing writer. + */ + public @NonNull NormalizedMetadataWriter write(final NormalizedNode data, final NormalizedMetadata metadata) + throws IOException { + final PathArgument dataId = data.getIdentifier(); + final PathArgument metaId = metadata.getIdentifier(); + checkArgument(dataId.equals(metaId), "Mismatched data %s and metadata %s", dataId, metaId); + + final NormalizedMetadataStreamWriter metaWriter = writer.getExtensions() + .getInstance(NormalizedMetadataStreamWriter.class); + final NormalizedNodeStreamWriter delegate = metaWriter == null ? writer + : new NormalizedNodeStreamWriterMetadataDecorator(writer, metaWriter, metadata); + + final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(delegate, orderKeyLeaves); + nnWriter.write(data); + nnWriter.flush(); + return this; + } + + @Override + public void close() throws IOException { + writer.flush(); + writer.close(); + } + + @Override + public void flush() throws IOException { + writer.flush(); + } +} diff --git a/yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedNodeStreamWriterMetadataDecorator.java b/yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedNodeStreamWriterMetadataDecorator.java new file mode 100644 index 0000000000..9f8bfbe05b --- /dev/null +++ b/yang/rfc7952-data-util/src/main/java/org/opendaylight/yangtools/rfc7952/data/util/NormalizedNodeStreamWriterMetadataDecorator.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019 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.yangtools.rfc7952.data.util; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.Map.Entry; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata; +import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadataContainer; +import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadataStreamWriter; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; + +/** + * A simple decorator on top of a NormalizedNodeStreamWriter, which attaches NormalizedMetadata to the event stream, + * so that the metadata is emitted along with data. + */ +final class NormalizedNodeStreamWriterMetadataDecorator extends ForwardingNormalizedNodeStreamWriter { + private final Deque stack = new ArrayDeque<>(); + private final NormalizedMetadataStreamWriter metaWriter; + private final NormalizedNodeStreamWriter writer; + private final NormalizedMetadata metadata; + + NormalizedNodeStreamWriterMetadataDecorator(final NormalizedNodeStreamWriter writer, + final NormalizedMetadataStreamWriter metaWriter, final NormalizedMetadata metadata) { + this.writer = requireNonNull(writer); + this.metaWriter = requireNonNull(metaWriter); + this.metadata = requireNonNull(metadata); + } + + @Override + protected NormalizedNodeStreamWriter delegate() { + return writer; + } + + @Override + public void startLeafNode(final NodeIdentifier name) throws IOException { + super.startLeafNode(name); + enterMetadataNode(name); + } + + @Override + public void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startLeafSet(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startOrderedLeafSet(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startLeafSetEntryNode(final NodeWithValue name) throws IOException { + super.startLeafSetEntryNode(name); + enterMetadataNode(name); + } + + @Override + public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startContainerNode(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startUnkeyedList(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startUnkeyedListItem(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startMapNode(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint) + throws IOException { + super.startMapEntryNode(identifier, childSizeHint); + enterMetadataNode(identifier); + } + + @Override + public void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startOrderedMapNode(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startChoiceNode(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startChoiceNode(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void startAugmentationNode(final AugmentationIdentifier identifier) throws IOException { + super.startAugmentationNode(identifier); + enterMetadataNode(identifier); + } + + @Override + public void startAnyxmlNode(final NodeIdentifier name) throws IOException { + super.startAnyxmlNode(name); + enterMetadataNode(name); + } + + @Override + public void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint) throws IOException { + super.startYangModeledAnyXmlNode(name, childSizeHint); + enterMetadataNode(name); + } + + @Override + public void endNode() throws IOException { + super.endNode(); + stack.pop(); + } + + private void enterMetadataNode(final PathArgument name) throws IOException { + final NormalizedMetadata child = findMetadata(name); + if (child != null) { + emitAnnotations(child.getAnnotations()); + } + stack.push(child); + } + + private @Nullable NormalizedMetadata findMetadata(final PathArgument name) { + final NormalizedMetadata current = stack.peek(); + if (current instanceof NormalizedMetadataContainer) { + return ((NormalizedMetadataContainer) current).getChild(name).orElse(null); + } + + // This may either be the first entry or unattached metadata nesting + return stack.isEmpty() ? metadata : null; + } + + private void emitAnnotations(final Map annotations) throws IOException { + if (!annotations.isEmpty()) { + metaWriter.startMetadata(annotations.size()); + for (Entry entry : annotations.entrySet()) { + metaWriter.startMetadataEntry(entry.getKey(), null); + writer.nodeValue(entry.getValue()); + writer.endNode(); + } + writer.endNode(); + } + } +} -- 2.36.6