Make ListenerAdapter serialize JSON directly 14/93214/9
authorTomas Cere <tomas.cere@pantheon.tech>
Thu, 25 Jun 2020 09:00:47 +0000 (11:00 +0200)
committerRobert Varga <nite@hq.sk>
Wed, 4 Nov 2020 15:36:02 +0000 (15:36 +0000)
Change websocket notification serialization to serialize
to json directly instead of first serializing to xml and
subsequently converting to json.
While we are here fixup the websocket notification format
so it actually conforms to the sal-remote.yang model and
encode namespaces according to the json-ietf standard
instead of bundling an xmlns node in the json ouptut.

JIRA: NETCONF-471
Change-Id: I1b90c99980c0692b217523640bd6f67050f21e14
Signed-off-by: Tomas Cere <tomas.cere@pantheon.tech>
38 files changed:
restconf/restconf-common/pom.xml
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatterFactory.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatterFactory.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONDataTreeCandidateFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONNotificationFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatterFactory.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLDataTreeCandidateFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLNotificationFormatter.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/AbstractWebsocketSerializer.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/JsonDataTreeCandidateSerializer.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/XmlDataTreeCandidateSerializer.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/pom.xml
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerAdapter.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapperTest.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/JsonNotificationListenerTest.java [moved from restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerTest.java with 94% similarity]
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/XmlNotificationListenerTest.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-delete.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-delete.xml [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json
restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.xml [new file with mode: 0644]

index 23cdff21b598b08403180b4c7b3e709b8e0424fa..2893439a6efc45428d442df11d42c88cbc61be22 100644 (file)
       <groupId>org.opendaylight.yangtools</groupId>
       <artifactId>yang-data-api</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>yang-data-codec-gson</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>yang-data-codec-xml</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.opendaylight.yangtools</groupId>
       <artifactId>yang-data-impl</artifactId>
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatter.java
new file mode 100644 (file)
index 0000000..547c7bc
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import static org.opendaylight.restconf.common.formatters.NotificationFormatter.XML_OUTPUT_FACTORY;
+
+import com.google.common.annotations.Beta;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/**
+ * Base formatter for DataTreeCandidates which only handles exporting to a document for filter checking purpose.
+ */
+@Beta
+public abstract class DataTreeCandidateFormatter extends EventFormatter<Collection<DataTreeCandidate>> {
+
+    protected DataTreeCandidateFormatter() {
+    }
+
+    public DataTreeCandidateFormatter(String xpathFilter) throws XPathExpressionException {
+        super(xpathFilter);
+    }
+
+    @Override
+    final void fillDocument(Document doc, EffectiveModelContext schemaContext, Collection<DataTreeCandidate> input)
+            throws IOException {
+        final Element notificationElement = doc.createElementNS("urn:ietf:params:xml:ns:netconf:notification:1.0",
+                "notification");
+        final Element eventTimeElement = doc.createElement("eventTime");
+        eventTimeElement.setTextContent(toRFC3339(Instant.now()));
+        notificationElement.appendChild(eventTimeElement);
+
+        final Element notificationEventElement = doc.createElementNS(
+                "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
+
+        for (DataTreeCandidate candidate : input) {
+            final Element dataChangedElement = doc.createElement("data-changed-event");
+            try {
+                final Element dataElement = doc.createElement("data");
+                final DOMResult domResult = new DOMResult(dataElement);
+                final XMLStreamWriter writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(domResult);
+
+                final SchemaPath path = SchemaPath.create(candidate.getRootPath().getPathArguments().stream()
+                        .filter(p -> !(p instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
+                        .map(YangInstanceIdentifier.PathArgument::getNodeType).collect(Collectors.toList()), true);
+
+                writeCandidate(XMLStreamNormalizedNodeStreamWriter.create(writer, schemaContext,
+                        path), candidate);
+
+                dataChangedElement.appendChild(dataElement);
+            } catch (final XMLStreamException e) {
+                throw new IOException("Failed to write notification content", e);
+            }
+            notificationElement.appendChild(notificationEventElement);
+        }
+        doc.appendChild(notificationElement);
+    }
+
+    static void writeCandidate(final NormalizedNodeStreamWriter writer, final DataTreeCandidate candidate)
+            throws IOException {
+        if (candidate.getRootNode().getDataAfter().isPresent()) {
+            try (NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(writer)) {
+                nodeWriter.write(candidate.getRootNode().getDataAfter().get());
+            }
+        }
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatterFactory.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatterFactory.java
new file mode 100644 (file)
index 0000000..d5b15fb
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import java.util.Collection;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+
+public interface DataTreeCandidateFormatterFactory extends EventFormatterFactory<Collection<DataTreeCandidate>> {
+
+    @Override
+    DataTreeCandidateFormatter getFormatter();
+
+    @Override
+    DataTreeCandidateFormatter getFormatter(String xpathFilter) throws XPathExpressionException;
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatter.java
new file mode 100644 (file)
index 0000000..dc64825
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.w3c.dom.Document;
+
+public abstract class EventFormatter<T> implements Immutable {
+    private static final XPathFactory XPF = XPathFactory.newInstance();
+
+    // FIXME: NETCONF-369: XPath operates without namespace context, therefore we need an namespace-unaware builder.
+    //        Once it is fixed we can use UntrustedXML instead.
+    private static final @NonNull DocumentBuilderFactory DBF;
+
+    static {
+        final DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
+        f.setCoalescing(true);
+        f.setExpandEntityReferences(false);
+        f.setIgnoringElementContentWhitespace(true);
+        f.setIgnoringComments(true);
+        f.setXIncludeAware(false);
+        try {
+            f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+            f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+            f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+            f.setFeature("http://xml.org/sax/features/external-general-entities", false);
+            f.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+        } catch (final ParserConfigurationException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+        DBF = f;
+    }
+
+    private final XPathExpression filter;
+
+    EventFormatter()  {
+        this.filter = null;
+    }
+
+    EventFormatter(final String xpathFilter)  throws XPathExpressionException {
+        final XPath xpath;
+        synchronized (XPF) {
+            xpath = XPF.newXPath();
+        }
+        // FIXME: NETCONF-369: we need to bind the namespace context here and for that we need the SchemaContext
+        filter = xpath.compile(xpathFilter);
+    }
+
+    public final Optional<String> eventData(final EffectiveModelContext schemaContext, final T input, final Instant now,
+                                            boolean leafNodesOnly, boolean skipData)
+            throws Exception {
+        if (!filterMatches(schemaContext, input, now)) {
+            return Optional.empty();
+        }
+        return Optional.of(createText(schemaContext, input, now, leafNodesOnly, skipData));
+    }
+
+    /**
+     * Export the provided input into the provided document so we can verify whether a filter matches the content.
+     *
+     * @param doc the document to fill
+     * @param schemaContext context to use for the export
+     * @param input data to export
+     * @throws IOException if any IOException occurs during export to the document
+     */
+    abstract void fillDocument(Document doc, EffectiveModelContext schemaContext, T input) throws IOException;
+
+    /**
+     * Format the input data into string representation of the data provided.
+     *
+     * @param schemaContext context to use for the export
+     * @param input input data
+     * @param now time the event happened
+     * @param leafNodesOnly option to include only leaves in the result
+     * @param skipData option to skip data in the result, only paths would be included
+     * @return String representation of the formatted data
+     * @throws Exception if the underlying formatters fail to export the data to the requested format
+     */
+    abstract String createText(EffectiveModelContext schemaContext, T input, Instant now, boolean leafNodesOnly,
+                               boolean skipData) throws Exception;
+
+    private boolean filterMatches(final EffectiveModelContext schemaContext, final T input, final Instant now)
+            throws IOException {
+        if (filter == null) {
+            return true;
+        }
+
+        final Document doc;
+        try {
+            doc = DBF.newDocumentBuilder().newDocument();
+        } catch (final ParserConfigurationException e) {
+            throw new IOException("Failed to create a new document", e);
+        }
+        fillDocument(doc, schemaContext, input);
+
+        final Boolean eval;
+        try {
+            eval = (Boolean) filter.evaluate(doc, XPathConstants.BOOLEAN);
+        } catch (final XPathExpressionException e) {
+            throw new IllegalStateException("Failed to evaluate expression " + filter, e);
+        }
+
+        return eval.booleanValue();
+    }
+
+    /**
+     * Formats data specified by RFC3339.
+     *
+     * @param now time stamp
+     * @return Data specified by RFC3339.
+     */
+    static String toRFC3339(final Instant now) {
+        return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(OffsetDateTime.ofInstant(now, ZoneId.systemDefault()));
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatterFactory.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatterFactory.java
new file mode 100644 (file)
index 0000000..b586881
--- /dev/null
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import javax.xml.xpath.XPathExpressionException;
+
+public interface EventFormatterFactory<T> {
+
+    EventFormatter<T> getFormatter();
+
+    EventFormatter<T> getFormatter(String xpathFilter) throws XPathExpressionException;
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONDataTreeCandidateFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONDataTreeCandidateFormatter.java
new file mode 100644 (file)
index 0000000..38c585a
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.time.Instant;
+import java.util.Collection;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.restconf.common.serializer.JsonDataTreeCandidateSerializer;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class JSONDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
+    private static final Logger LOG = LoggerFactory.getLogger(JSONDataTreeCandidateFormatter.class);
+    public static final String SAL_REMOTE_NAMESPACE = "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote";
+    public static final String NETCONF_NOTIFICATION_NAMESPACE = "urn-ietf-params-xml-ns-netconf-notification-1.0";
+    private final JSONCodecFactorySupplier codecSupplier;
+
+    private JSONDataTreeCandidateFormatter(final JSONCodecFactorySupplier codecSupplier) {
+        this.codecSupplier = requireNonNull(codecSupplier);
+    }
+
+    private JSONDataTreeCandidateFormatter(final String xpathFilter, final JSONCodecFactorySupplier codecSupplier)
+            throws XPathExpressionException {
+        super(xpathFilter);
+        this.codecSupplier = requireNonNull(codecSupplier);
+    }
+
+    public static DataTreeCandidateFormatterFactory createFactory(
+            final JSONCodecFactorySupplier codecSupplier) {
+        requireNonNull(codecSupplier);
+        return new DataTreeCandidateFormatterFactory() {
+            @Override
+            public DataTreeCandidateFormatter getFormatter(final String xpathFilter)
+                    throws XPathExpressionException {
+                return new JSONDataTreeCandidateFormatter(xpathFilter, codecSupplier);
+            }
+
+            @Override
+            public DataTreeCandidateFormatter getFormatter() {
+                return new JSONDataTreeCandidateFormatter(codecSupplier);
+            }
+        };
+    }
+
+    @Override
+    String createText(final EffectiveModelContext schemaContext, final Collection<DataTreeCandidate> input,
+                      final Instant now, boolean leafNodesOnly, boolean skipData)
+            throws IOException {
+        final Writer writer = new StringWriter();
+        final JsonWriter jsonWriter = new JsonWriter(writer).beginObject();
+
+        jsonWriter.name(NETCONF_NOTIFICATION_NAMESPACE + ":notification").beginObject();
+        jsonWriter.name(SAL_REMOTE_NAMESPACE + ":data-changed-notification").beginObject();
+        jsonWriter.name("data-change-event").beginArray();
+
+        final JsonDataTreeCandidateSerializer serializer =
+                new JsonDataTreeCandidateSerializer(codecSupplier, schemaContext, jsonWriter);
+        for (final DataTreeCandidate candidate : input) {
+            serializer.serialize(candidate, leafNodesOnly, skipData);
+        }
+
+        // data-change-event
+        jsonWriter.endArray();
+        // data-changed-notification
+        jsonWriter.endObject();
+
+        jsonWriter.name("event-time").value(toRFC3339(now));
+        jsonWriter.endObject();
+
+        // notification
+        jsonWriter.endObject();
+
+        return writer.toString();
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONNotificationFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONNotificationFormatter.java
new file mode 100644 (file)
index 0000000..a0f5a44
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.time.Instant;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+public final class JSONNotificationFormatter extends NotificationFormatter {
+    private final JSONCodecFactorySupplier codecSupplier;
+
+    private JSONNotificationFormatter(final JSONCodecFactorySupplier codecSupplier) {
+        this.codecSupplier = requireNonNull(codecSupplier);
+    }
+
+    private JSONNotificationFormatter(final String xpathFilter, final JSONCodecFactorySupplier codecSupplier)
+            throws XPathExpressionException {
+        super(xpathFilter);
+        this.codecSupplier = requireNonNull(codecSupplier);
+    }
+
+    public static NotificationFormatterFactory createFactory(final JSONCodecFactorySupplier codecSupplier) {
+        requireNonNull(codecSupplier);
+        return new NotificationFormatterFactory() {
+            @Override
+            public JSONNotificationFormatter getFormatter(final String xpathFilter)
+                    throws XPathExpressionException {
+                return new JSONNotificationFormatter(xpathFilter, codecSupplier);
+            }
+
+            @Override
+            public JSONNotificationFormatter getFormatter() {
+                return new JSONNotificationFormatter(codecSupplier);
+            }
+        };
+    }
+
+    @Override
+    String createText(final EffectiveModelContext schemaContext, final DOMNotification input, final Instant now,
+                      boolean leafNodesOnly, boolean skipData)
+            throws IOException {
+        final Writer writer = new StringWriter();
+        final JsonWriter jsonWriter = new JsonWriter(writer).beginObject();
+        jsonWriter.name("ietf-restconf:notification").beginObject();
+        writeNotificationBody(JSONNormalizedNodeStreamWriter.createNestedWriter(
+                codecSupplier.getShared(schemaContext), input.getType(), null, jsonWriter), input.getBody());
+        jsonWriter.endObject();
+        jsonWriter.name("event-time").value(toRFC3339(now)).endObject();
+        jsonWriter.close();
+        return writer.toString();
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatter.java
new file mode 100644 (file)
index 0000000..49381fd
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import java.io.IOException;
+import java.time.Instant;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.mdsal.dom.api.DOMEvent;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+public abstract class NotificationFormatter extends EventFormatter<DOMNotification> {
+    protected static final XMLOutputFactory XML_OUTPUT_FACTORY;
+
+    static {
+        XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory();
+        XML_OUTPUT_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
+    }
+
+    NotificationFormatter() {
+
+    }
+
+    public NotificationFormatter(final String xpathFilter) throws XPathExpressionException {
+        super(xpathFilter);
+    }
+
+    @Override
+    void fillDocument(Document doc, EffectiveModelContext schemaContext, DOMNotification input) throws IOException {
+        final Element notificationElement = doc.createElementNS("urn:ietf:params:xml:ns:netconf:notification:1.0",
+                "notification");
+        final Element eventTimeElement = doc.createElement("eventTime");
+        if (input instanceof DOMEvent) {
+            eventTimeElement.setTextContent(toRFC3339(((DOMEvent) input).getEventInstant()));
+        } else {
+            eventTimeElement.setTextContent(toRFC3339(Instant.now()));
+        }
+        notificationElement.appendChild(eventTimeElement);
+
+        final Element notificationEventElement = doc.createElementNS(
+                "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "create-notification-stream");
+        final Element dataElement = doc.createElement("notification");
+        final DOMResult result = new DOMResult(dataElement);
+        try {
+            final XMLStreamWriter writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(result);
+            try {
+                writeNotificationBody(XMLStreamNormalizedNodeStreamWriter.create(writer, schemaContext,
+                        input.getType()), input.getBody());
+            } finally {
+                writer.close();
+            }
+        } catch (final XMLStreamException e) {
+            throw new IOException("Failed to write notification content", e);
+        }
+        notificationElement.appendChild(notificationEventElement);
+        doc.appendChild(notificationElement);
+    }
+
+    static void writeNotificationBody(final NormalizedNodeStreamWriter writer, final ContainerNode body)
+            throws IOException {
+        try (NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(writer)) {
+            nodeWriter.write(body);
+        }
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatterFactory.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatterFactory.java
new file mode 100644 (file)
index 0000000..9d30b9e
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+
+public interface NotificationFormatterFactory extends EventFormatterFactory<DOMNotification> {
+    @Override
+    NotificationFormatter getFormatter();
+
+    @Override
+    NotificationFormatter getFormatter(String xpathFilter) throws XPathExpressionException;
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLDataTreeCandidateFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLDataTreeCandidateFormatter.java
new file mode 100644 (file)
index 0000000..404da42
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import static org.opendaylight.restconf.common.formatters.NotificationFormatter.XML_OUTPUT_FACTORY;
+import static org.opendaylight.restconf.common.formatters.XMLNotificationFormatter.NOTIFICATION_ELEMENT;
+import static org.opendaylight.restconf.common.formatters.XMLNotificationFormatter.NOTIFICATION_NAMESPACE;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.util.Collection;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.restconf.common.serializer.XmlDataTreeCandidateSerializer;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+public final class XMLDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
+    private static final XMLDataTreeCandidateFormatter INSTANCE = new XMLDataTreeCandidateFormatter();
+
+    public static final DataTreeCandidateFormatterFactory FACTORY =
+        new DataTreeCandidateFormatterFactory() {
+            @Override
+            public XMLDataTreeCandidateFormatter getFormatter(final String xpathFilter)
+                    throws XPathExpressionException {
+                return new XMLDataTreeCandidateFormatter(xpathFilter);
+            }
+
+            @Override
+            public XMLDataTreeCandidateFormatter getFormatter() {
+                return INSTANCE;
+            }
+        };
+
+    private XMLDataTreeCandidateFormatter() {
+    }
+
+    private XMLDataTreeCandidateFormatter(final String xpathFilter) throws XPathExpressionException {
+        super(xpathFilter);
+    }
+
+    @Override
+    String createText(EffectiveModelContext schemaContext, Collection<DataTreeCandidate> input, Instant now,
+                      boolean leafNodesOnly, boolean skipData)
+            throws Exception {
+        StringWriter writer = new StringWriter();
+
+        final XMLStreamWriter xmlStreamWriter;
+        try {
+            xmlStreamWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(writer);
+            xmlStreamWriter.setDefaultNamespace(NOTIFICATION_NAMESPACE);
+
+            xmlStreamWriter.writeStartElement(NOTIFICATION_NAMESPACE, NOTIFICATION_ELEMENT);
+            xmlStreamWriter.writeDefaultNamespace(NOTIFICATION_NAMESPACE);
+
+            xmlStreamWriter.writeStartElement("eventTime");
+            xmlStreamWriter.writeCharacters(toRFC3339(now));
+            xmlStreamWriter.writeEndElement();
+
+            xmlStreamWriter.writeStartElement("data-changed-notification");
+
+            final XmlDataTreeCandidateSerializer serializer =
+                    new XmlDataTreeCandidateSerializer(schemaContext, xmlStreamWriter);
+
+            for (final DataTreeCandidate candidate : input) {
+                serializer.serialize(candidate, leafNodesOnly, skipData);
+            }
+
+            // data-changed-notification
+            xmlStreamWriter.writeEndElement();
+
+            // notification
+            xmlStreamWriter.writeEndElement();
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to write notification content", e);
+        }
+
+
+        return writer.toString();
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLNotificationFormatter.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLNotificationFormatter.java
new file mode 100644 (file)
index 0000000..d2af552
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020 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.common.formatters;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.time.Instant;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+public class XMLNotificationFormatter extends NotificationFormatter {
+    private static final XMLNotificationFormatter INSTANCE = new XMLNotificationFormatter();
+
+    static final String NOTIFICATION_NAMESPACE = "urn:ietf:params:xml:ns:netconf:notification:1.0";
+    static final String NOTIFICATION_ELEMENT = "notification";
+
+    public static final NotificationFormatterFactory FACTORY = new NotificationFormatterFactory() {
+        @Override
+        public XMLNotificationFormatter getFormatter(final String xpathFilter) throws XPathExpressionException {
+            return new XMLNotificationFormatter(xpathFilter);
+        }
+
+        @Override
+        public XMLNotificationFormatter getFormatter() {
+            return INSTANCE;
+        }
+    };
+
+    public XMLNotificationFormatter() {
+    }
+
+    public XMLNotificationFormatter(String xpathFilter) throws XPathExpressionException {
+        super(xpathFilter);
+    }
+
+    @Override
+    String createText(EffectiveModelContext schemaContext, DOMNotification input, Instant now,
+                      boolean leafNodesOnly, boolean skipData)
+            throws IOException {
+        final StringWriter writer = new StringWriter();
+        try {
+            final XMLStreamWriter xmlStreamWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(writer);
+            xmlStreamWriter.setDefaultNamespace(NOTIFICATION_NAMESPACE);
+
+            xmlStreamWriter.writeStartElement(NOTIFICATION_NAMESPACE, NOTIFICATION_ELEMENT);
+            xmlStreamWriter.writeDefaultNamespace(NOTIFICATION_NAMESPACE);
+
+            xmlStreamWriter.writeStartElement("eventTime");
+            xmlStreamWriter.writeCharacters(toRFC3339(now));
+            xmlStreamWriter.writeEndElement();
+
+            final NormalizedNodeStreamWriter nnStreamWriter =
+                    XMLStreamNormalizedNodeStreamWriter.create(xmlStreamWriter, schemaContext, input.getType());
+
+            final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(nnStreamWriter);
+            nnWriter.write(input.getBody());
+            nnWriter.flush();
+
+            xmlStreamWriter.writeEndElement();
+            xmlStreamWriter.writeEndDocument();
+            xmlStreamWriter.flush();
+
+            nnWriter.close();
+
+            return writer.toString();
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to write notification content", e);
+        }
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/AbstractWebsocketSerializer.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/AbstractWebsocketSerializer.java
new file mode 100644 (file)
index 0000000..9188009
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2020 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.common.serializer;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Set;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractWebsocketSerializer<T extends Exception> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AbstractWebsocketSerializer.class);
+
+    public void serialize(DataTreeCandidate candidate, boolean leafNodesOnly, boolean skipData) throws T {
+        final Deque<PathArgument> path = new ArrayDeque<>();
+        path.addAll(candidate.getRootPath().getPathArguments());
+        if (leafNodesOnly) {
+            serializeLeafNodesOnly(path, candidate.getRootNode(), skipData);
+            return;
+        }
+
+        serializeData(path, candidate.getRootNode(), skipData);
+    }
+
+    void serializeLeafNodesOnly(Deque<PathArgument> path, DataTreeCandidateNode candidate, boolean skipData)
+            throws T {
+        NormalizedNode<?, ?> node = null;
+        switch (candidate.getModificationType()) {
+            case UNMODIFIED:
+                // no reason to do anything with an unmodified node
+                LOG.debug("DataTreeCandidate for a notification is unmodified, not serializing leaves. Candidate: {}",
+                        candidate);
+                break;
+            case SUBTREE_MODIFIED:
+            case WRITE:
+            case APPEARED:
+                node = candidate.getDataAfter().get();
+                break;
+            case DELETE:
+            case DISAPPEARED:
+                node = candidate.getDataBefore().get();
+                break;
+            default:
+                LOG.error("DataTreeCandidate modification has unknown type: {}", candidate.getModificationType());
+        }
+
+        if (node == null) {
+            return;
+        }
+
+        if (node instanceof LeafNode<?> || node instanceof LeafSetNode) {
+            serializeData(path, candidate, skipData);
+            return;
+        }
+
+        for (DataTreeCandidateNode childNode : candidate.getChildNodes()) {
+            path.add(childNode.getIdentifier());
+            serializeLeafNodesOnly(path, childNode, skipData);
+            path.removeLast();
+        }
+    }
+
+    abstract void serializeData(Collection<PathArgument> path, DataTreeCandidateNode candidate, boolean skipData)
+            throws T;
+
+    abstract void serializePath(Collection<PathArgument> pathArguments) throws T;
+
+    abstract void serializeOperation(DataTreeCandidateNode candidate) throws T;
+
+    String convertPath(Collection<PathArgument> path) {
+        final StringBuilder pathBuilder = new StringBuilder();
+
+        for (PathArgument pathArgument : path) {
+            pathBuilder.append("/");
+            pathBuilder.append(pathArgument.getNodeType().getNamespace().toString().replaceAll(":", "-"));
+            pathBuilder.append(":");
+            pathBuilder.append(pathArgument.getNodeType().getLocalName());
+
+            if (pathArgument instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
+                pathBuilder.append("[");
+                final Set<Map.Entry<QName, Object>> keys =
+                        ((YangInstanceIdentifier.NodeIdentifierWithPredicates) pathArgument).entrySet();
+                for (Map.Entry<QName, Object> key : keys) {
+                    pathBuilder.append(key.getKey().getNamespace().toString().replaceAll(":", "-"));
+                    pathBuilder.append(":");
+                    pathBuilder.append(key.getKey().getLocalName());
+                    pathBuilder.append("='");
+                    pathBuilder.append(key.getValue().toString());
+                    pathBuilder.append("'");
+                }
+                pathBuilder.append("]");
+            }
+        }
+
+        return pathBuilder.toString();
+    }
+
+    String modificationTypeToOperation(DataTreeCandidateNode candidate, ModificationType modificationType) {
+        switch (modificationType) {
+            case UNMODIFIED:
+                // shouldn't ever happen since the root of a modification is only triggered by some event
+                LOG.warn("DataTreeCandidate for a notification is unmodified. Candidate: {}", candidate);
+                return "none";
+            case SUBTREE_MODIFIED:
+            case WRITE:
+            case APPEARED:
+                if (candidate.getDataBefore().isPresent()) {
+                    return "updated";
+                } else {
+                    return "created";
+                }
+            case DELETE:
+            case DISAPPEARED:
+                return "deleted";
+            default:
+                LOG.error("DataTreeCandidate modification has unknown type: {}",
+                        candidate.getModificationType());
+                throw new IllegalStateException("Unknown modification type");
+        }
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/JsonDataTreeCandidateSerializer.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/JsonDataTreeCandidateSerializer.java
new file mode 100644 (file)
index 0000000..37fb4ad
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2020 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.common.serializer;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+public class JsonDataTreeCandidateSerializer extends AbstractWebsocketSerializer<IOException> {
+
+    private final JSONCodecFactorySupplier codecSupplier;
+    private final EffectiveModelContext context;
+    private final JsonWriter jsonWriter;
+
+    public JsonDataTreeCandidateSerializer(final JSONCodecFactorySupplier codecSupplier,
+                                           final EffectiveModelContext context,
+                                           final JsonWriter jsonWriter) {
+
+        this.codecSupplier = codecSupplier;
+        this.context = context;
+        this.jsonWriter = jsonWriter;
+    }
+
+    void serializeData(Collection<YangInstanceIdentifier.PathArgument> nodePath, DataTreeCandidateNode candidate,
+                       boolean skipData)
+            throws IOException {
+        final SchemaPath path = SchemaPath.create(nodePath.stream()
+                .filter(p -> !(p instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
+                .map(YangInstanceIdentifier.PathArgument::getNodeType).collect(Collectors.toList()), true);
+        final NormalizedNodeStreamWriter nestedWriter =
+                JSONNormalizedNodeStreamWriter
+                        .createNestedWriter(codecSupplier.getShared(context), path.getParent(), null, jsonWriter);
+
+        jsonWriter.beginObject();
+        serializePath(nodePath);
+
+        if (!skipData && candidate.getDataAfter().isPresent()) {
+            jsonWriter.name("data").beginObject();
+            NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(nestedWriter);
+            nodeWriter.write(candidate.getDataAfter().get());
+            nodeWriter.flush();
+
+            // end data
+            jsonWriter.endObject();
+        }
+
+        serializeOperation(candidate);
+        jsonWriter.endObject();
+    }
+
+    @Override
+    void serializeOperation(final DataTreeCandidateNode candidate)
+            throws IOException {
+        jsonWriter.name("operation").value(modificationTypeToOperation(candidate, candidate.getModificationType()));
+    }
+
+    @Override
+    void serializePath(Collection<YangInstanceIdentifier.PathArgument> pathArguments)
+            throws IOException {
+        jsonWriter.name("path").value(convertPath(pathArguments));
+    }
+}
diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/XmlDataTreeCandidateSerializer.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/XmlDataTreeCandidateSerializer.java
new file mode 100644 (file)
index 0000000..ea65fae
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2020 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.common.serializer;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+public class XmlDataTreeCandidateSerializer extends AbstractWebsocketSerializer<Exception> {
+
+    private final EffectiveModelContext context;
+    private final XMLStreamWriter xmlWriter;
+
+    public XmlDataTreeCandidateSerializer(EffectiveModelContext context, XMLStreamWriter xmlWriter) {
+        this.context = context;
+        this.xmlWriter = xmlWriter;
+    }
+
+    @Override
+    void serializeData(Collection<PathArgument> nodePath, DataTreeCandidateNode candidate, boolean skipData)
+            throws Exception {
+        final SchemaPath path = SchemaPath.create(nodePath.stream()
+                .filter(p -> !(p instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
+                .map(PathArgument::getNodeType).collect(Collectors.toList()), true);
+        final NormalizedNodeStreamWriter nodeStreamWriter =
+                XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, context, path.getParent());
+
+        xmlWriter.writeStartElement("data-changed-event");
+        serializePath(nodePath);
+
+        if (!skipData && candidate.getDataAfter().isPresent()) {
+            xmlWriter.writeStartElement("data");
+            NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(nodeStreamWriter);
+            nnWriter.write(candidate.getDataAfter().get());
+            nnWriter.flush();
+
+            xmlWriter.writeEndElement();
+        }
+        serializeOperation(candidate);
+
+        xmlWriter.writeEndElement();
+    }
+
+    @Override
+    public void serializePath(Collection<PathArgument> pathArguments) throws XMLStreamException {
+        xmlWriter.writeStartElement("path");
+        xmlWriter.writeCharacters(convertPath(pathArguments));
+        xmlWriter.writeEndElement();
+    }
+
+    @Override
+    public void serializeOperation(DataTreeCandidateNode candidate) throws XMLStreamException {
+        xmlWriter.writeStartElement("operation");
+        xmlWriter.writeCharacters(modificationTypeToOperation(candidate, candidate.getModificationType()));
+        xmlWriter.writeEndElement();
+    }
+}
index 62b6efcfb634a2ff0bb3d9e06d2ffb5004b6142e..3a5d54ceda96f5a158e7b98e8b8375124ed11303 100644 (file)
       <artifactId>threadpool-config-impl</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.xmlunit</groupId>
+      <artifactId>xmlunit-assertj</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.javacrumbs.json-unit</groupId>
+      <artifactId>json-unit-assertj</artifactId>
+      <version>2.18.1</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
index 655e48bc39c1c15436c5d43a79ce8249e146f746..a237f17f73ad584ce0ef3cfe5487e093689e55da 100644 (file)
@@ -8,42 +8,26 @@
 package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
-import java.io.IOException;
 import java.time.Instant;
 import java.util.Collection;
-import java.util.Map.Entry;
 import java.util.Optional;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.transform.dom.DOMResult;
-import org.json.XML;
+import java.util.stream.Collectors;
+import javax.xml.xpath.XPathExpressionException;
 import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
+import org.opendaylight.restconf.common.formatters.DataTreeCandidateFormatter;
+import org.opendaylight.restconf.common.formatters.DataTreeCandidateFormatterFactory;
+import org.opendaylight.restconf.common.formatters.JSONDataTreeCandidateFormatter;
+import org.opendaylight.restconf.common.formatters.XMLDataTreeCandidateFormatter;
 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
-import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-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.LeafNode;
-import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
-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.tree.DataTreeCandidate;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
-import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
-import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.Module;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
-import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
 
 /**
  * {@link ListenerAdapter} is responsible to track events, which occurred by changing data in data source.
@@ -51,14 +35,18 @@ import org.w3c.dom.Node;
 public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
 
     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
-    private static final String DATA_CHANGE_EVENT = "data-change-event";
+//    private static final String DATA_CHANGE_EVENT = "data-change-event";
     private static final String PATH = "path";
-    private static final String OPERATION = "operation";
+//    private static final String OPERATION = "operation";
+    private static final DataTreeCandidateFormatterFactory JSON_FORMATTER_FACTORY =
+            JSONDataTreeCandidateFormatter.createFactory(JSONCodecFactorySupplier.RFC7951);
 
     private final YangInstanceIdentifier path;
     private final String streamName;
     private final NotificationOutputType outputType;
 
+    @VisibleForTesting DataTreeCandidateFormatter formatter;
+
     /**
      * Creates new {@link ListenerAdapter} listener specified by path and stream name and register for subscribing.
      *
@@ -74,18 +62,57 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
         this.path = requireNonNull(path);
         this.streamName = requireNonNull(streamName);
         checkArgument(!streamName.isEmpty());
+
+        formatter = getFormatterFactory().getFormatter();
+    }
+
+    private DataTreeCandidateFormatterFactory getFormatterFactory() {
+        switch (outputType) {
+            case JSON:
+                return JSON_FORMATTER_FACTORY;
+            case XML:
+                return XMLDataTreeCandidateFormatter.FACTORY;
+            default:
+                throw new IllegalArgumentException(("Unsupported outputType" + outputType));
+        }
+    }
+
+    private DataTreeCandidateFormatter getFormatter(final String filter) throws XPathExpressionException {
+        final DataTreeCandidateFormatterFactory factory = getFormatterFactory();
+        return filter == null || filter.isEmpty() ? factory.getFormatter() : factory.getFormatter(filter);
     }
 
     @Override
+    public void setQueryParams(final Instant start, final Instant stop, final String filter,
+                               final boolean leafNodesOnly, final boolean skipNotificationData) {
+        super.setQueryParams(start, stop, filter, leafNodesOnly, skipNotificationData);
+        try {
+            this.formatter = getFormatter(filter);
+        } catch (final XPathExpressionException e) {
+            throw new IllegalArgumentException("Failed to get filter", e);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("checkstyle:IllegalCatch")
     public void onDataTreeChanged(final Collection<DataTreeCandidate> dataTreeCandidates) {
         final Instant now = Instant.now();
         if (!checkStartStop(now, this)) {
             return;
         }
 
-        final String xml = prepareXml(dataTreeCandidates);
-        if (checkFilter(xml)) {
-            prepareAndPostData(xml);
+        final Optional<String> maybeData;
+        try {
+            maybeData = formatter.eventData(schemaHandler.get(), dataTreeCandidates, now, getLeafNodesOnly(),
+                    isSkipNotificationData());
+        } catch (final Exception e) {
+            LOG.error("Failed to process notification {}",
+                    dataTreeCandidates.stream().map(Object::toString).collect(Collectors.joining(",")), e);
+            return;
+        }
+
+        if (maybeData.isPresent()) {
+            post(maybeData.get());
         }
     }
 
@@ -113,285 +140,6 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements Cluster
         return this.path;
     }
 
-    /**
-     * Prepare data of notification and data to client.
-     *
-     * @param xml XML-formatted data.
-     */
-    private void prepareAndPostData(final String xml) {
-        if (this.outputType.equals(NotificationOutputType.JSON)) {
-            post(XML.toJSONObject(xml).toString());
-        } else {
-            post(xml);
-        }
-    }
-
-    /**
-     * Prepare data in printable form and transform it to String.
-     *
-     * @param dataTreeCandidates Data-tree candidates to be transformed.
-     * @return Data in printable form.
-     */
-    private String prepareXml(final Collection<DataTreeCandidate> dataTreeCandidates) {
-        final EffectiveModelContext schemaContext = schemaHandler.get();
-        final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext);
-        final Document doc = createDocument();
-        final Element notificationElement = basePartDoc(doc);
-
-        final Element dataChangedNotificationEventElement = doc.createElementNS(
-                "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
-
-        addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, dataTreeCandidates,
-                schemaContext, dataContextTree);
-        notificationElement.appendChild(dataChangedNotificationEventElement);
-        return transformDoc(doc);
-    }
-
-    /**
-     * Adds values to data changed notification event element.
-     */
-    private void addValuesToDataChangedNotificationEventElement(final Document doc,
-            final Element dataChangedNotificationEventElement, final Collection<DataTreeCandidate> dataTreeCandidates,
-            final EffectiveModelContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
-
-        for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) {
-            DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode();
-            if (candidateNode == null) {
-                continue;
-            }
-            YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
-            boolean isSkipNotificationData = this.isSkipNotificationData();
-            if (isSkipNotificationData) {
-                createCreatedChangedDataChangeEventElementWithoutData(doc, dataChangedNotificationEventElement,
-                        dataTreeCandidate.getRootNode(), schemaContext);
-            } else {
-                addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
-                        yiid.getParent(), schemaContext, dataSchemaContextTree);
-            }
-        }
-    }
-
-    private void addNodeToDataChangeNotificationEventElement(final Document doc,
-            final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
-            final YangInstanceIdentifier parentYiid, final EffectiveModelContext schemaContext,
-            final DataSchemaContextTree dataSchemaContextTree) {
-
-        Optional<NormalizedNode<?, ?>> optionalNormalizedNode = Optional.empty();
-        switch (candidateNode.getModificationType()) {
-            case APPEARED:
-            case SUBTREE_MODIFIED:
-            case WRITE:
-                optionalNormalizedNode = candidateNode.getDataAfter();
-                break;
-            case DELETE:
-            case DISAPPEARED:
-                optionalNormalizedNode = candidateNode.getDataBefore();
-                break;
-            case UNMODIFIED:
-            default:
-                break;
-        }
-
-        if (!optionalNormalizedNode.isPresent()) {
-            LOG.error("No node present in notification for {}", candidateNode);
-            return;
-        }
-
-        NormalizedNode<?, ?> normalizedNode = optionalNormalizedNode.get();
-        YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid)
-                .append(normalizedNode.getIdentifier()).build();
-
-        final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(yiid);
-        checkState(childrenSchemaNode.isPresent());
-        boolean isNodeMixin = childrenSchemaNode.get().isMixin();
-        boolean isSkippedNonLeaf = getLeafNodesOnly() && !(normalizedNode instanceof LeafNode);
-        if (!isNodeMixin && !isSkippedNonLeaf) {
-            Node node = null;
-            switch (candidateNode.getModificationType()) {
-                case APPEARED:
-                case SUBTREE_MODIFIED:
-                case WRITE:
-                    Operation op = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
-                    node = createCreatedChangedDataChangeEventElement(doc, yiid, normalizedNode, op,
-                            schemaContext, dataSchemaContextTree);
-                    break;
-                case DELETE:
-                case DISAPPEARED:
-                    node = createDataChangeEventElement(doc, yiid, schemaContext, Operation.DELETED);
-                    break;
-                case UNMODIFIED:
-                default:
-                    break;
-            }
-            if (node != null) {
-                dataChangedNotificationEventElement.appendChild(node);
-            }
-        }
-
-        for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) {
-            addNodeToDataChangeNotificationEventElement(
-                    doc, dataChangedNotificationEventElement, childNode, yiid, schemaContext, dataSchemaContextTree);
-        }
-    }
-
-    /**
-     * Creates data-changed event element from data.
-     *
-     * @param doc           {@link Document}
-     * @param schemaContext Schema context.
-     * @param operation  Operation value
-     * @return {@link Node} represented by changed event element.
-     */
-    private static Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
-            final SchemaContext schemaContext, Operation operation) {
-        final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
-        final Element pathElement = doc.createElement(PATH);
-        addPathAsValueToElement(eventPath, pathElement, schemaContext);
-        dataChangeEventElement.appendChild(pathElement);
-
-        final Element operationElement = doc.createElement(OPERATION);
-        operationElement.setTextContent(operation.value);
-        dataChangeEventElement.appendChild(operationElement);
-
-        return dataChangeEventElement;
-    }
-
-    /**
-     * Creates data change notification element without data element.
-     *
-     * @param doc
-     *       {@link Document}
-     * @param dataChangedNotificationEventElement
-     *       {@link Element}
-     * @param candidateNode
-     *       {@link DataTreeCandidateNode}
-     */
-    private void createCreatedChangedDataChangeEventElementWithoutData(final Document doc,
-            final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
-            final SchemaContext schemaContext) {
-        final Operation operation;
-        switch (candidateNode.getModificationType()) {
-            case APPEARED:
-            case SUBTREE_MODIFIED:
-            case WRITE:
-                operation = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
-                break;
-            case DELETE:
-            case DISAPPEARED:
-                operation = Operation.DELETED;
-                break;
-            case UNMODIFIED:
-            default:
-                return;
-        }
-        Node dataChangeEventElement = createDataChangeEventElement(doc, getPath(), schemaContext, operation);
-        dataChangedNotificationEventElement.appendChild(dataChangeEventElement);
-    }
-
-    private Node createCreatedChangedDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
-            final NormalizedNode<?, ?> normalized, final Operation operation, final EffectiveModelContext schemaContext,
-            final DataSchemaContextTree dataSchemaContextTree) {
-        final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
-        final Element pathElement = doc.createElement(PATH);
-        addPathAsValueToElement(eventPath, pathElement, schemaContext);
-        dataChangeEventElement.appendChild(pathElement);
-
-        final Element operationElement = doc.createElement("operation");
-        operationElement.setTextContent(operation.value);
-        dataChangeEventElement.appendChild(operationElement);
-
-        try {
-            final SchemaPath nodePath;
-            final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(eventPath);
-            checkState(childrenSchemaNode.isPresent());
-            if (normalized instanceof MapEntryNode || normalized instanceof UnkeyedListEntryNode) {
-                nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath();
-            } else {
-                nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath().getParent();
-            }
-            final DOMResult domResult = writeNormalizedNode(normalized, schemaContext, nodePath);
-            final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
-            final Element dataElement = doc.createElement("data");
-            dataElement.appendChild(result);
-            dataChangeEventElement.appendChild(dataElement);
-        } catch (final IOException e) {
-            LOG.error("Error in writer ", e);
-        } catch (final XMLStreamException e) {
-            LOG.error("Error processing stream", e);
-        }
-
-        return dataChangeEventElement;
-    }
-
-    /**
-     * Adds path as value to element.
-     *
-     * @param eventPath     Path to data in data store.
-     * @param element       {@link Element}
-     * @param schemaContext Schema context.
-     */
-    private static void addPathAsValueToElement(final YangInstanceIdentifier eventPath, final Element element,
-            final SchemaContext schemaContext) {
-        final StringBuilder textContent = new StringBuilder();
-
-        for (final PathArgument pathArgument : eventPath.getPathArguments()) {
-            if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
-                continue;
-            }
-            textContent.append("/");
-            writeIdentifierWithNamespacePrefix(textContent, pathArgument.getNodeType(), schemaContext);
-            if (pathArgument instanceof NodeIdentifierWithPredicates) {
-                for (final Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) pathArgument).entrySet()) {
-                    final QName keyValue = entry.getKey();
-                    final String predicateValue = String.valueOf(entry.getValue());
-                    textContent.append("[");
-                    writeIdentifierWithNamespacePrefix(textContent, keyValue, schemaContext);
-                    textContent.append("='").append(predicateValue).append("']");
-                }
-            } else if (pathArgument instanceof NodeWithValue) {
-                textContent.append("[.='").append(((NodeWithValue<?>) pathArgument).getValue()).append("']");
-            }
-        }
-        element.setTextContent(textContent.toString());
-    }
-
-    /**
-     * Writes identifier that consists of prefix and QName.
-     *
-     * @param textContent   Text builder that should be supplemented by QName and its modules name.
-     * @param qualifiedName QName of the element.
-     * @param schemaContext Schema context that holds modules which should contain module specified in QName.
-     */
-    private static void writeIdentifierWithNamespacePrefix(final StringBuilder textContent, final QName qualifiedName,
-            final SchemaContext schemaContext) {
-        final Optional<Module> module = schemaContext.findModule(qualifiedName.getModule());
-        if (module.isPresent()) {
-            textContent.append(module.get().getName());
-            textContent.append(":");
-            textContent.append(qualifiedName.getLocalName());
-        } else {
-            LOG.error("Cannot write identifier with namespace prefix in data-change listener adapter: "
-                    + "Cannot find module in schema context for input QName {}.", qualifiedName);
-            throw new IllegalStateException(String.format("Cannot find module in schema context for input QName %s.",
-                    qualifiedName));
-        }
-    }
-
-    /**
-     * Consists of three types {@link Operation#CREATED}, {@link Operation#UPDATED} and {@link Operation#DELETED}.
-     */
-    private enum Operation {
-        CREATED("created"),
-        UPDATED("updated"),
-        DELETED("deleted");
-
-        private final String value;
-
-        Operation(final String value) {
-            this.value = value;
-        }
-    }
-
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
index c1cbdd98f80ac53ac1e8dc4904fb2d48b7218c35..9745083dc58907be22621d11dbbb89873c225943 100644 (file)
@@ -12,29 +12,20 @@ import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
 import java.time.Instant;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.transform.dom.DOMResult;
+import java.util.Optional;
+import javax.xml.xpath.XPathExpressionException;
 import org.opendaylight.mdsal.dom.api.DOMNotification;
 import org.opendaylight.mdsal.dom.api.DOMNotificationListener;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
-import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.restconf.common.formatters.JSONNotificationFormatter;
+import org.opendaylight.restconf.common.formatters.NotificationFormatter;
+import org.opendaylight.restconf.common.formatters.NotificationFormatterFactory;
+import org.opendaylight.restconf.common.formatters.XMLNotificationFormatter;
+import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
-import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
-import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
 
 /**
  * {@link NotificationListenerAdapter} is responsible to track events on notifications.
@@ -42,10 +33,15 @@ import org.w3c.dom.Node;
 public class NotificationListenerAdapter extends AbstractCommonSubscriber implements DOMNotificationListener {
 
     private static final Logger LOG = LoggerFactory.getLogger(NotificationListenerAdapter.class);
+    private static final NotificationFormatterFactory JSON_FORMATTER_FACTORY = JSONNotificationFormatter.createFactory(
+            JSONCodecFactorySupplier.RFC7951);
 
     private final String streamName;
     private final Absolute path;
-    private final String outputType;
+    private final NotificationOutputType outputType;
+
+    @VisibleForTesting NotificationFormatter formatter;
+
 
     /**
      * Set path of listener and stream name.
@@ -57,10 +53,41 @@ public class NotificationListenerAdapter extends AbstractCommonSubscriber implem
     NotificationListenerAdapter(final Absolute path, final String streamName, final String outputType) {
         setLocalNameOfPath(path.lastNodeIdentifier().getLocalName());
 
-        this.outputType = requireNonNull(outputType);
+        this.outputType = NotificationOutputType.forName(requireNonNull(outputType)).get();
         this.path = requireNonNull(path);
         this.streamName = requireNonNull(streamName);
         checkArgument(!streamName.isEmpty());
+
+        LOG.info("output type: {}, {}", outputType, this.outputType);
+
+        this.formatter = getFormatterFactory().getFormatter();
+    }
+
+    private NotificationFormatterFactory getFormatterFactory() {
+        switch (outputType) {
+            case JSON:
+                return JSON_FORMATTER_FACTORY;
+            case XML:
+                return XMLNotificationFormatter.FACTORY;
+            default:
+                throw new IllegalArgumentException(("Unsupported outputType" + outputType));
+        }
+    }
+
+    private NotificationFormatter getFormatter(final String filter) throws XPathExpressionException {
+        NotificationFormatterFactory factory = getFormatterFactory();
+        return filter == null || filter.isEmpty() ? factory.getFormatter() : factory.getFormatter(filter);
+    }
+
+    @Override
+    public void setQueryParams(Instant start, Instant stop, String filter, boolean leafNodesOnly,
+                               boolean skipNotificationData) {
+        super.setQueryParams(start, stop, filter, leafNodesOnly, skipNotificationData);
+        try {
+            this.formatter = getFormatter(filter);
+        } catch (XPathExpressionException e) {
+            throw new IllegalArgumentException("Failed to get filter", e);
+        }
     }
 
     /**
@@ -70,20 +97,27 @@ public class NotificationListenerAdapter extends AbstractCommonSubscriber implem
      */
     @Override
     public String getOutputType() {
-        return this.outputType;
+        return this.outputType.getName();
     }
 
     @Override
+    @SuppressWarnings("checkstyle:IllegalCatch")
     public void onNotification(final DOMNotification notification) {
         final Instant now = Instant.now();
         if (!checkStartStop(now, this)) {
             return;
         }
 
-        final EffectiveModelContext schemaContext = schemaHandler.get();
-        final String xml = prepareXml(schemaContext, notification);
-        if (checkFilter(xml)) {
-            post(outputType.equals("JSON") ? prepareJson(schemaContext, notification) : xml);
+        final Optional<String> maybeOutput;
+        try {
+            maybeOutput = formatter.eventData(schemaHandler.get(), notification, now, getLeafNodesOnly(),
+                    isSkipNotificationData());
+        } catch (Exception e) {
+            LOG.error("Failed to process notification {}", notification, e);
+            return;
+        }
+        if (maybeOutput.isPresent()) {
+            post(maybeOutput.get());
         }
     }
 
@@ -106,68 +140,6 @@ public class NotificationListenerAdapter extends AbstractCommonSubscriber implem
         return this.path;
     }
 
-    /**
-     * Creation of JSON from notification data.
-     *
-     * @return Transformed notification data in JSON format.
-     */
-    @VisibleForTesting
-    String prepareJson(final EffectiveModelContext schemaContext, final DOMNotification notification) {
-        final JsonParser jsonParser = new JsonParser();
-        final JsonObject json = new JsonObject();
-        json.add("ietf-restconf:notification", jsonParser.parse(writeBodyToString(schemaContext, notification)));
-        json.addProperty("event-time", ListenerAdapter.toRFC3339(Instant.now()));
-        return json.toString();
-    }
-
-    private static String writeBodyToString(final EffectiveModelContext schemaContext,
-            final DOMNotification notification) {
-        final Writer writer = new StringWriter();
-        final NormalizedNodeStreamWriter jsonStream = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
-                JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(schemaContext),
-                notification.getType(), null, JsonWriterFactory.createJsonWriter(writer));
-        final NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(jsonStream);
-        try {
-            nodeWriter.write(notification.getBody());
-            nodeWriter.close();
-        } catch (final IOException e) {
-            throw new RestconfDocumentedException("Problem while writing body of notification to JSON. ", e);
-        }
-        return writer.toString();
-    }
-
-    /**
-     * Creation of XML from notification data.
-     *
-     * @return Transformed notification data in XML format.
-     */
-    private String prepareXml(final EffectiveModelContext schemaContext, final DOMNotification notification) {
-        final Document doc = createDocument();
-        final Element notificationElement = basePartDoc(doc);
-
-        final Element notificationEventElement = doc.createElementNS(
-                "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "create-notification-stream");
-        addValuesToNotificationEventElement(doc, notificationEventElement, schemaContext, notification);
-        notificationElement.appendChild(notificationEventElement);
-
-        return transformDoc(doc);
-    }
-
-    private void addValuesToNotificationEventElement(final Document doc, final Element element,
-            final EffectiveModelContext schemaContext, final DOMNotification notification) {
-        try {
-            final DOMResult domResult = writeNormalizedNode(notification.getBody(), schemaContext, path.asSchemaPath());
-            final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
-            final Element dataElement = doc.createElement("notification");
-            dataElement.appendChild(result);
-            element.appendChild(dataElement);
-        } catch (final IOException e) {
-            LOG.error("Error in writer ", e);
-        } catch (final XMLStreamException e) {
-            LOG.error("Error processing stream", e);
-        }
-    }
-
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
index dd4ab693dbacea8eff428098217d5d7335b82156..c555b9297884abfed9cb0847022c3caaba5f059e 100644 (file)
@@ -19,6 +19,7 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.XML;
 import org.junit.Assert;
@@ -288,7 +289,7 @@ public class RestconfDocumentedExceptionMapperTest {
     public Response expectedResponse;
 
     @Test
-    public void testMappingOfExceptionToResponse() {
+    public void testMappingOfExceptionToResponse() throws JSONException {
         exceptionMapper.setHttpHeaders(httpHeaders);
         final Response response = exceptionMapper.toResponse(thrownException);
         compareResponseWithExpectation(expectedResponse, response);
@@ -301,7 +302,8 @@ public class RestconfDocumentedExceptionMapperTest {
         return httpHeaders;
     }
 
-    private static void compareResponseWithExpectation(final Response expectedResponse, final Response actualResponse) {
+    private static void compareResponseWithExpectation(final Response expectedResponse, final Response actualResponse)
+            throws JSONException {
         final String errorMessage = String.format("Actual response %s doesn't equal to expected response %s",
                 actualResponse, expectedResponse);
         Assert.assertEquals(errorMessage, expectedResponse.getStatus(), actualResponse.getStatus());
@@ -7,7 +7,6 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
 
-import static java.util.Objects.requireNonNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -15,6 +14,7 @@ import static org.mockito.Mockito.when;
 
 import com.google.common.collect.Lists;
 import java.net.URI;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Optional;
@@ -42,9 +42,13 @@ import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
-public class NotificationListenerTest {
+public class JsonNotificationListenerTest {
+    private static final Logger LOG = LoggerFactory.getLogger(JsonNotificationListenerTest.class);
+
     private static final QNameModule MODULE = QNameModule.create(URI.create("notifi:mod"), Revision.of("2016-11-23"));
 
     private static EffectiveModelContext SCHEMA_CONTEXT;
@@ -73,6 +77,8 @@ public class NotificationListenerTest {
 
         final String result = prepareJson(notificationData, schemaPathNotifi);
 
+        LOG.info("json result: {}", result);
+
         assertTrue(result.contains("ietf-restconf:notification"));
         assertTrue(result.contains("event-time"));
         assertTrue(result.contains("notifi-module:notifi-leaf"));
@@ -216,9 +222,10 @@ public class NotificationListenerTest {
         return child;
     }
 
-    private static String prepareJson(final DOMNotification notificationData, final Absolute schemaPathNotifi) {
+    private static String prepareJson(final DOMNotification notificationData, final Absolute schemaPathNotifi)
+            throws Exception {
         final NotificationListenerAdapter notifiAdapter = ListenersBroker.getInstance().registerNotificationListener(
-                schemaPathNotifi, "stream-name", NotificationOutputType.JSON);
-        return requireNonNull(notifiAdapter.prepareJson(SCHEMA_CONTEXT, notificationData));
+                schemaPathNotifi, "json-stream", NotificationOutputType.JSON);
+        return notifiAdapter.formatter.eventData(SCHEMA_CONTEXT, notificationData, Instant.now(), false, false).get();
     }
 }
index 0cbf42d7f17950bd22d000a7ff63311fff9ec529..7381680d4cb1cb6bf81416b56a9032b9c043b9d5 100644 (file)
@@ -8,6 +8,7 @@
 package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
 
 import static java.time.Instant.EPOCH;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.common.util.concurrent.Uninterruptibles;
@@ -19,6 +20,7 @@ import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -47,6 +49,7 @@ import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
 import org.skyscreamer.jsonassert.JSONAssert;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.xmlunit.assertj.XmlAssert;
 
 public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapterTest.class);
@@ -64,6 +67,21 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
     private static final String JSON_NOTIF_WITHOUT_DATA_DELETE =
             "/listener-adapter-test/notif-without-data-del.json";
 
+    private static final String XML_NOTIF_CREATE = "/listener-adapter-test/notif-create.xml";
+    private static final String XML_NOTIF_UPDATE =  "/listener-adapter-test/notif-update.xml";
+    private static final String XML_NOTIF_DEL =  "/listener-adapter-test/notif-delete.xml";
+
+    private static final String XML_NOTIF_LEAVES_CREATE = "/listener-adapter-test/notif-leaves-create.xml";
+    private static final String XML_NOTIF_LEAVES_UPDATE =  "/listener-adapter-test/notif-leaves-update.xml";
+    private static final String XML_NOTIF_LEAVES_DEL =  "/listener-adapter-test/notif-leaves-delete.xml";
+
+    private static final String XML_NOTIF_WITHOUT_DATA_CREATE =
+            "/listener-adapter-test/notif-without-data-create.xml";
+    private static final String XML_NOTIF_WITHOUT_DATA_UPDATE =
+            "/listener-adapter-test/notif-without-data-update.xml";
+    private static final String XML_NOTIF_WITHOUT_DATA_DELETE =
+            "/listener-adapter-test/notif-without-data-delete.xml";
+
     private static final YangInstanceIdentifier PATCH_CONT_YIID =
             YangInstanceIdentifier.create(new YangInstanceIdentifier.NodeIdentifier(PatchCont.QNAME));
 
@@ -113,51 +131,86 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
             notificationLatch.countDown();
         }
 
-        public void assertGot(final String json) {
-            if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 5, TimeUnit.SECONDS)) {
+        public void assertGot(final String json) throws JSONException {
+            if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 500, TimeUnit.SECONDS)) {
                 fail("Timed out waiting for notification for: " + json);
             }
 
             LOG.info("lastNotification: {}", lastNotification);
-            String withFakeDate = withFakeDate(lastNotification);
+            final String withFakeDate = withFakeDate(lastNotification);
             LOG.info("Comparing: \n{}\n{}", json, withFakeDate);
 
             JSONAssert.assertEquals(json, withFakeDate, false);
             this.lastNotification = null;
             notificationLatch = new CountDownLatch(1);
         }
+
+        public void assertXmlSimilar(String xml) {
+            awaitUntillNotification(xml);
+
+            LOG.info("lastNotification: {}", lastNotification);
+            final String withFakeDate = withFakeXmlDate(lastNotification);
+            LOG.info("Comparing: \n{}\n{}", xml, withFakeDate);
+
+            XmlAssert.assertThat(xml).and(withFakeDate).ignoreWhitespace().ignoreChildNodesOrder().areSimilar();
+            this.lastNotification = null;
+            notificationLatch = new CountDownLatch(1);
+        }
+
+        public String awaitUntillNotification(String xml) {
+            if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 500, TimeUnit.SECONDS)) {
+                fail("Timed out waiting for notification for: " + xml);
+            }
+            return lastNotification;
+        }
+
+        public void resetLatch() {
+            notificationLatch = new CountDownLatch(1);
+        }
     }
 
-    static String withFakeDate(final String in) {
-        JSONObject doc = new JSONObject(in);
-        JSONObject notification = doc.getJSONObject("notification");
+    static String withFakeDate(final String in) throws JSONException {
+        final JSONObject doc = new JSONObject(in);
+        final JSONObject notification =
+                doc.getJSONObject("urn-ietf-params-xml-ns-netconf-notification-1.0:notification");
         if (notification == null) {
             return in;
         }
-        notification.put("eventTime", "someDate");
+        notification.put("event-time", "someDate");
         return doc.toString();
     }
 
-    private String getNotifJson(final String path) throws IOException, URISyntaxException {
-        URL url = getClass().getResource(path);
-        byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
+    static String withFakeXmlDate(String in) {
+        return in.replaceAll("<eventTime>.*</eventTime>", "<eventTime>someDate</eventTime>");
+    }
+
+    private String getNotifJson(final String path) throws IOException, URISyntaxException, JSONException {
+        final URL url = getClass().getResource(path);
+        final byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
         return withFakeDate(new String(bytes, StandardCharsets.UTF_8));
     }
 
+    private String getResultXml(final String path) throws IOException, URISyntaxException, JSONException {
+        final URL url = getClass().getResource(path);
+        final byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
+        return withFakeXmlDate(new String(bytes, StandardCharsets.UTF_8));
+    }
+
     @Test
     public void testJsonNotifsLeaves() throws Exception {
         ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
                 NotificationOutputTypeGrouping.NotificationOutputType.JSON, true, false);
         adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
 
-        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+        final DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
                 .getInstance(DOMDataTreeChangeService.class);
-        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        final DOMDataTreeIdentifier root =
+                new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
         changeService.registerDataTreeChangeListener(root, adapter);
 
         WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
         MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
-        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+        final InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
                 .child(MyList1.class, new MyList1Key("Althea"));
         writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
         writeTransaction.commit();
@@ -181,14 +234,15 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
                 NotificationOutputTypeGrouping.NotificationOutputType.JSON, false, false);
         adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
 
-        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+        final DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
                 .getInstance(DOMDataTreeChangeService.class);
-        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        final DOMDataTreeIdentifier root =
+                new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
         changeService.registerDataTreeChangeListener(root, adapter);
 
         WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
         MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
-        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+        final InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
                 .child(MyList1.class, new MyList1Key("Althea"));
         writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
         writeTransaction.commit();
@@ -235,4 +289,99 @@ public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest {
         writeTransaction.commit();
         adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_DELETE));
     }
+
+    @Test
+    public void testXmlNotifications() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                NotificationOutputTypeGrouping.NotificationOutputType.XML, false, false);
+        adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+                .getInstance(DOMDataTreeChangeService.class);
+        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        changeService.registerDataTreeChangeListener(root, adapter);
+        WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+        MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+                .child(MyList1.class, new MyList1Key("Althea"));
+        writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_DEL));
+    }
+
+    @Test
+    public void testXmlSkipData() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                NotificationOutputTypeGrouping.NotificationOutputType.XML, false, true);
+        adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+                .getInstance(DOMDataTreeChangeService.class);
+        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        changeService.registerDataTreeChangeListener(root, adapter);
+        WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+        MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+                .child(MyList1.class, new MyList1Key("Althea"));
+        writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_WITHOUT_DATA_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_WITHOUT_DATA_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_WITHOUT_DATA_DELETE));
+    }
+
+    @Test
+    public void testXmlLeavesOnly() throws Exception {
+        ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+                NotificationOutputTypeGrouping.NotificationOutputType.XML, true, false);
+        adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+        DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+                .getInstance(DOMDataTreeChangeService.class);
+        DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+        changeService.registerDataTreeChangeListener(root, adapter);
+        WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+        MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+        InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+                .child(MyList1.class, new MyList1Key("Althea"));
+        writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_LEAVES_CREATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+        writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+        writeTransaction.commit();
+        adapter.assertXmlSimilar(getResultXml(XML_NOTIF_LEAVES_UPDATE));
+
+        writeTransaction = dataBroker.newWriteOnlyTransaction();
+        writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+        writeTransaction.commit();
+
+        // xmlunit cannot compare deeper children it seems out of the box so just check the iid encoding
+        final String notification = adapter.awaitUntillNotification("");
+        assertTrue(notification.contains("instance-identifier-patch-module:my-leaf12"));
+        assertTrue(notification.contains("instance-identifier-patch-module:my-leaf11"));
+        assertTrue(notification.contains("instance-identifier-patch-module:name"));
+    }
 }
diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/XmlNotificationListenerTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/XmlNotificationListenerTest.java
new file mode 100644 (file)
index 0000000..1f2cf65
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2020 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.nb.rfc8040.streams.listeners;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+
+import com.google.common.collect.Lists;
+import java.net.URI;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.nb.rfc8040.TestUtils;
+import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping;
+import org.opendaylight.yangtools.util.SingletonSet;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.xmlunit.assertj.XmlAssert;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class XmlNotificationListenerTest {
+    private static final QNameModule MODULE = QNameModule.create(URI.create("notifi:mod"), Revision.of("2016-11-23"));
+
+    private static EffectiveModelContext SCHEMA_CONTEXT;
+
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+        SCHEMA_CONTEXT = TestUtils.loadSchemaContext("/notifications");
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        SCHEMA_CONTEXT = null;
+    }
+
+    @Test
+    public void notifi_leafTest() throws Exception {
+        final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-leaf"));
+
+        final DOMNotification notificationData = mock(DOMNotification.class);
+
+        final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+        final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), leaf);
+
+        when(notificationData.getType()).thenReturn(schemaPathNotifi);
+        when(notificationData.getBody()).thenReturn(notifiBody);
+
+        final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+        final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+                + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-leaf xmlns=\"notifi:mod\">"
+                + "<lf>value</lf></notifi-leaf></notification>";
+
+        assertXmlMatches(result, control);
+    }
+
+    @Test
+    public void notifi_cont_leafTest() throws Exception {
+        final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-cont"));
+
+        final DOMNotification notificationData = mock(DOMNotification.class);
+
+        final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+        final ContainerNode cont = mockCont(QName.create(MODULE, "cont"), leaf);
+        final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), cont);
+
+        when(notificationData.getType()).thenReturn(schemaPathNotifi);
+        when(notificationData.getBody()).thenReturn(notifiBody);
+
+        final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+        final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+                + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-cont xmlns=\"notifi:mod\">"
+                + "<cont><lf>value</lf></cont></notifi-cont></notification>";
+
+        assertXmlMatches(result, control);
+    }
+
+    @Test
+    public void notifi_list_Test() throws Exception {
+        final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-list"));
+
+        final DOMNotification notificationData = mock(DOMNotification.class);
+
+        final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+        final MapEntryNode entry = mockMapEntry(QName.create(MODULE, "lst"), leaf);
+        final MapNode list = mockList(QName.create(MODULE, "lst"), entry);
+        final ContainerNode cont = mockCont(QName.create(MODULE, "cont"), list);
+        final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), cont);
+
+        when(notificationData.getType()).thenReturn(schemaPathNotifi);
+        when(notificationData.getBody()).thenReturn(notifiBody);
+
+        final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+        final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+                + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-list xmlns=\"notifi:mod\">"
+                + "<notifi-list><lf><lf>value</lf></lf></notifi-list></notifi-list></notification>";
+
+        assertXmlMatches(result, control);
+    }
+
+    @Test
+    public void notifi_grpTest() throws Exception {
+        final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-grp"));
+
+        final DOMNotification notificationData = mock(DOMNotification.class);
+
+        final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+        final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), leaf);
+
+        when(notificationData.getType()).thenReturn(schemaPathNotifi);
+        when(notificationData.getBody()).thenReturn(notifiBody);
+
+        final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+        final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+                + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-grp xmlns=\"notifi:mod\">"
+                + "<lf>value</lf></notifi-grp></notification>";
+
+        assertXmlMatches(result, control);
+    }
+
+    @Test
+    public void notifi_augmTest() throws Exception {
+        final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-augm"));
+
+        final DOMNotification notificationData = mock(DOMNotification.class);
+
+        final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf-augm"));
+        final AugmentationNode augm = mockAugm(leaf);
+        final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), augm);
+
+        when(notificationData.getType()).thenReturn(schemaPathNotifi);
+        when(notificationData.getBody()).thenReturn(notifiBody);
+
+        final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+        final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+                + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-augm xmlns=\"notifi:mod\">"
+                + "<lf-augm>value</lf-augm></notifi-augm></notification>";
+
+        assertXmlMatches(result, control);
+    }
+
+    private static void assertXmlMatches(String result, String control) {
+        XmlAssert.assertThat(result).and(control)
+                // text values have localName null but we want to compare those, ignore only nodes that have localName
+                // with eventTime value
+                .withNodeFilter(node -> node.getLocalName() == null || !node.getLocalName().equals("eventTime"))
+                .areSimilar();
+    }
+
+    private static AugmentationNode mockAugm(final LeafNode<String> leaf) {
+        final AugmentationNode augm = mock(AugmentationNode.class);
+        final AugmentationIdentifier augmId = new AugmentationIdentifier(SingletonSet.of(leaf.getNodeType()));
+        when(augm.getIdentifier()).thenReturn(augmId);
+
+        final Collection<DataContainerChild<? extends PathArgument, ?>> childs = new ArrayList<>();
+        childs.add(leaf);
+
+        when(augm.getValue()).thenReturn(childs);
+        return augm;
+    }
+
+    private static MapEntryNode mockMapEntry(final QName entryQName, final LeafNode<String> leaf) {
+        final MapEntryNode entry = mock(MapEntryNode.class);
+        final NodeIdentifierWithPredicates nodeId = NodeIdentifierWithPredicates.of(leaf.getNodeType(),
+                leaf.getNodeType(), "value");
+        when(entry.getIdentifier()).thenReturn(nodeId);
+        when(entry.getChild(any())).thenReturn(Optional.of(leaf));
+
+        final Collection<DataContainerChild<? extends PathArgument, ?>> childs = new ArrayList<>();
+        childs.add(leaf);
+
+        when(entry.getValue()).thenReturn(childs);
+        return entry;
+    }
+
+    private static MapNode mockList(final QName listQName, final MapEntryNode... entries) {
+        final MapNode list = mock(MapNode.class);
+        when(list.getIdentifier()).thenReturn(NodeIdentifier.create(listQName));
+        when(list.getValue()).thenReturn(Lists.newArrayList(entries));
+        return list;
+    }
+
+    private static ContainerNode mockCont(final QName contQName,
+                                          final DataContainerChild<? extends PathArgument, ?> child) {
+        final ContainerNode cont = mock(ContainerNode.class);
+        when(cont.getIdentifier()).thenReturn(NodeIdentifier.create(contQName));
+
+        final Collection<DataContainerChild<? extends PathArgument, ?>> childs = new ArrayList<>();
+        childs.add(child);
+        when(cont.getValue()).thenReturn(childs);
+        return cont;
+    }
+
+    private static LeafNode<String> mockLeaf(final QName leafQName) {
+        final LeafNode<String> child = mock(LeafNode.class);
+        when(child.getNodeType()).thenReturn(leafQName);
+        when(child.getIdentifier()).thenReturn(NodeIdentifier.create(leafQName));
+        when(child.getValue()).thenReturn("value");
+        return child;
+    }
+
+    private static String prepareXmlResult(final DOMNotification notificationData, final Absolute schemaPathNotifi)
+            throws Exception {
+        final NotificationListenerAdapter notifiAdapter = ListenersBroker.getInstance().registerNotificationListener(
+                schemaPathNotifi, "xml-stream", NotificationOutputTypeGrouping.NotificationOutputType.XML);
+        return notifiAdapter.formatter.eventData(SCHEMA_CONTEXT, notificationData, Instant.now(), false, false).get();
+    }
+}
index 31a7f1a412fab047216cdafce2f966e253498e9d..39748b97d610aafa39b52e0223ac0920ebfb54a1 100644 (file)
@@ -1,55 +1,23 @@
 {
-    "notification": {
-        "data-changed-notification": {
-            "data-change-event": [
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
                 {
-                    "data": {
-                        "my-leaf11": {
-                            "content": "Jed",
-                            "xmlns": "instance:identifier:patch:module"
+                    "path":"/instance-identifier-patch-module:patch-cont",
+                    "data":{
+                        "instance-identifier-patch-module:patch-cont":{
+                            "my-list1":[
+                                {
+                                    "name":"Althea",
+                                    "my-leaf11":"Jed"
+                                }
+                            ]
                         }
                     },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
-                },
-                {
-                    "data": {
-                        "name": {
-                            "content": "Althea",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
-                    },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
-                },
-                {
-                    "data": {
-                        "patch-cont": {
-                            "my-list1": {
-                                "my-leaf11": "Jed",
-                                "name": "Althea"
-                            },
-                            "xmlns": "instance:identifier:patch:module"
-                        }
-                    },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont"
-                },
-                {
-                    "data": {
-                        "my-list1": {
-                            "my-leaf11": "Jed",
-                            "name": "Althea",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
-                    },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']"
+                    "operation":"created"
                 }
-            ],
-            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+            ]
         },
-        "eventTime": "2017-09-17T13:32:03.586+03:00",
-        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+        "event-time":"2020-10-14T11:16:51.111635+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.xml
new file mode 100644 (file)
index 0000000..6f6678a
--- /dev/null
@@ -0,0 +1,17 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont</path>
+            <data>
+                <patch-cont xmlns="instance:identifier:patch:module">
+                    <my-list1>
+                        <name>Althea</name>
+                        <my-leaf11>Jed</my-leaf11>
+                    </my-list1>
+                </patch-cont>
+            </data>
+            <operation>created</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index a7cb21379d9fd50b60eb38fbb308f01fcf66d0bf..0c79ab64125a5ec299d3bf9bdd6b1c73ffd8acbb 100644 (file)
@@ -1,31 +1,13 @@
 {
-    "notification": {
-        "data-changed-notification": {
-            "data-change-event": [
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
                 {
-                    "operation": "deleted",
-                    "path": "/instance-identifier-patch-module:patch-cont"
-                },
-                {
-                    "operation": "deleted",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']"
-                },
-                {
-                    "operation": "deleted",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
-                },
-                {
-                    "operation": "deleted",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
-                },
-                {
-                    "operation": "deleted",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
+                    "path":"/instance-identifier-patch-module:patch-cont",
+                    "operation":"deleted"
                 }
-            ],
-            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+            ]
         },
-        "eventTime": "2017-09-17T14:18:53.404+03:00",
-        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+        "event-time":"2020-10-14T11:20:33.271836+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-delete.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-delete.xml
new file mode 100644 (file)
index 0000000..b66adc0
--- /dev/null
@@ -0,0 +1,9 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont</path>
+            <operation>deleted</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index f685b8960528256cabc8815c20f3dfe6816d15c1..c46115ef09270600e5cf064ca00156287fce7c5e 100644 (file)
@@ -1,31 +1,23 @@
 {
-    "notification": {
-        "data-changed-notification": {
-            "data-change-event": [
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
                 {
-                    "data": {
-                        "my-leaf11": {
-                            "content": "Jed",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
+                    "path":"/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11",
+                    "data":{
+                        "instance-identifier-patch-module:my-leaf11":"Jed"
                     },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11"
+                    "operation":"created"
                 },
                 {
-                    "data": {
-                        "name": {
-                            "content": "Althea",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
+                    "path":"/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name",
+                    "data":{
+                        "instance-identifier-patch-module:name":"Althea"
                     },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                    "operation":"created"
                 }
-            ],
-            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+            ]
         },
-        "eventTime": "2017-09-17T11:23:10.323+03:00",
-        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+        "event-time":"2020-10-15T13:01:29.019468+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.xml
new file mode 100644 (file)
index 0000000..b751d50
--- /dev/null
@@ -0,0 +1,19 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T13:50:24.917103+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11</path>
+            <data>
+                <my-leaf11 xmlns="instance:identifier:patch:module">Jed</my-leaf11>
+            </data>
+            <operation>created</operation>
+        </data-changed-event>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name</path>
+            <data>
+                <name xmlns="instance:identifier:patch:module">Althea</name>
+            </data>
+            <operation>created</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index fd1f1d8e0d534e8e841cfc4d7d47c91d65c74adc..b82d295f2414095620c81435af54b3e981bc81f1 100644 (file)
@@ -1,6 +1,6 @@
 {
-    "notification": {
-        "data-changed-notification": {
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
             "data-change-event": [
                 {
                     "operation": "deleted",
@@ -15,9 +15,7 @@
                     "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
                 }
             ],
-            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
         },
-        "eventTime": "2017-09-18T15:30:16.099+03:00",
-        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+        "event-time": "2017-09-18T15:30:16.099+03:00",
     }
 }
index 890d945f7e61cf4498e54d340e59bbef66a24c05..7a0b6161d3af6e118c89b08c32fb344d11bf06a6 100644 (file)
@@ -1,31 +1,23 @@
 {
-    "notification": {
-        "data-changed-notification": {
-            "data-change-event": [
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
                 {
-                    "data": {
-                        "my-leaf12": {
-                            "content": "Bertha",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
+                    "path":"/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name",
+                    "data":{
+                        "instance-identifier-patch-module:name":"Althea"
                     },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
+                    "operation":"updated"
                 },
                 {
-                    "data": {
-                        "name": {
-                            "content": "Althea",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
+                    "path":"/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12",
+                    "data":{
+                        "instance-identifier-patch-module:my-leaf12":"Bertha"
                     },
-                    "operation": "updated",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                    "operation":"created"
                 }
-            ],
-            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+            ]
         },
-        "eventTime": "2017-09-18T14:20:54.82+03:00",
-        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+        "event-time":"2020-10-15T13:23:29.520115+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.xml
new file mode 100644 (file)
index 0000000..183dd27
--- /dev/null
@@ -0,0 +1,19 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T13:54:31.86969+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name</path>
+            <data>
+                <name xmlns="instance:identifier:patch:module">Althea</name>
+            </data>
+            <operation>updated</operation>
+        </data-changed-event>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12</path>
+            <data>
+                <my-leaf12 xmlns="instance:identifier:patch:module">Bertha</my-leaf12>
+            </data>
+            <operation>created</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index 0ee0547f1b36b078d9df79326ae7fcd7eaee5c97..55dc269df8961c3e31be46f2c33ad67f11741ae7 100644 (file)
@@ -1,57 +1,24 @@
 {
-    "notification": {
-        "data-changed-notification": {
-            "data-change-event": [
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
                 {
-                    "data": {
-                        "patch-cont": {
-                            "my-list1": {
-                                "my-leaf11": "Jed",
-                                "my-leaf12": "Bertha",
-                                "name": "Althea"
-                            },
-                            "xmlns": "instance:identifier:patch:module"
+                    "path":"/instance-identifier-patch-module:patch-cont",
+                    "data":{
+                        "instance-identifier-patch-module:patch-cont":{
+                            "my-list1":[
+                                {
+                                    "name":"Althea",
+                                    "my-leaf11":"Jed",
+                                    "my-leaf12":"Bertha"
+                                }
+                            ]
                         }
                     },
-                    "operation": "updated",
-                    "path": "/instance-identifier-patch-module:patch-cont"
-                },
-                {
-                    "data": {
-                        "my-list1": {
-                            "my-leaf11": "Jed",
-                            "my-leaf12": "Bertha",
-                            "name": "Althea",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
-                    },
-                    "operation": "updated",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']"
-                },
-                {
-                    "data": {
-                        "my-leaf12": {
-                            "content": "Bertha",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
-                    },
-                    "operation": "created",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12"
-                },
-                {
-                    "data": {
-                        "name": {
-                            "content": "Althea",
-                            "xmlns": "instance:identifier:patch:module"
-                        }
-                    },
-                    "operation": "updated",
-                    "path": "/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name"
+                    "operation":"updated"
                 }
-            ],
-            "xmlns": "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"
+            ]
         },
-        "eventTime": "2017-09-18T15:52:25.213+03:00",
-        "xmlns": "urn:ietf:params:xml:ns:netconf:notification:1.0"
+        "event-time":"2020-10-14T11:19:34.549843+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.xml
new file mode 100644 (file)
index 0000000..b1f7953
--- /dev/null
@@ -0,0 +1,18 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T13:41:50.132897+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont</path>
+            <data>
+                <patch-cont xmlns="instance:identifier:patch:module">
+                    <my-list1>
+                        <name>Althea</name>
+                        <my-leaf11>Jed</my-leaf11>
+                        <my-leaf12>Bertha</my-leaf12>
+                    </my-list1>
+                </patch-cont>
+            </data>
+            <operation>updated</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index 6e4dadc36e2c46c3225b22450ff059963e3a332d..5ba501222380c38c281e3d25b28ed91164292870 100644 (file)
@@ -1,13 +1,13 @@
 {
-    "notification":{
-        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
-        "data-changed-notification":{
-            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
-            "data-change-event":{
-                "path":"/instance-identifier-patch-module:patch-cont",
-                "operation":"created"
-            }
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
+                {
+                    "path":"/instance-identifier-patch-module:patch-cont",
+                    "operation":"created"
+                }
+            ]
         },
-        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+        "event-time":"2020-10-14T12:34:59.085525+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.xml
new file mode 100644 (file)
index 0000000..29e051a
--- /dev/null
@@ -0,0 +1,9 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont</path>
+            <operation>created</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index dc3f739779efc0df9e2b1c509a9d3f7986c2e8f4..bcf15972a5004665312beb0fff59588a1a51f4c9 100644 (file)
@@ -1,13 +1,13 @@
 {
-    "notification":{
-        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
-        "data-changed-notification":{
-            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
-            "data-change-event":{
-                "path":"/instance-identifier-patch-module:patch-cont",
-                "operation":"deleted"
-            }
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
+                {
+                    "path":"/instance-identifier-patch-module:patch-cont",
+                    "operation":"deleted"
+                }
+            ]
         },
-        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+        "event-time":"2020-10-14T12:34:59.085525+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-delete.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-delete.xml
new file mode 100644 (file)
index 0000000..b66adc0
--- /dev/null
@@ -0,0 +1,9 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont</path>
+            <operation>deleted</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file
index c22c956216fb44e2c3cb979cfe2fb88770914548..703b978f9b4c16882a2141e9559e72fc20a0840e 100644 (file)
@@ -1,13 +1,13 @@
 {
-    "notification":{
-        "xmlns":"urn:ietf:params:xml:ns:netconf:notification:1.0",
-        "data-changed-notification":{
-            "xmlns":"urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote",
-            "data-change-event":{
-                "path":"/instance-identifier-patch-module:patch-cont",
-                "operation":"updated"
-            }
+    "urn-ietf-params-xml-ns-netconf-notification-1.0:notification":{
+        "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote:data-changed-notification":{
+            "data-change-event":[
+                {
+                    "path":"/instance-identifier-patch-module:patch-cont",
+                    "operation":"updated"
+                }
+            ]
         },
-        "eventTime":"2020-05-31T18:45:05.132101+05:30"
+        "event-time":"2020-10-14T12:34:59.085525+02:00"
     }
-}
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.xml b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.xml
new file mode 100644 (file)
index 0000000..51d8ada
--- /dev/null
@@ -0,0 +1,9 @@
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+    <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+    <data-changed-notification>
+        <data-changed-event>
+            <path>/instance-identifier-patch-module:patch-cont</path>
+            <operation>updated</operation>
+        </data-changed-event>
+    </data-changed-notification>
+</notification>
\ No newline at end of file