From 40ff777cd15b7146fea5c498a883065d60cb5bde Mon Sep 17 00:00:00 2001 From: Tomas Cere Date: Thu, 25 Jun 2020 11:00:47 +0200 Subject: [PATCH] Make ListenerAdapter serialize JSON directly 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 --- restconf/restconf-common/pom.xml | 8 + .../DataTreeCandidateFormatter.java | 87 +++++ .../DataTreeCandidateFormatterFactory.java | 21 + .../common/formatters/EventFormatter.java | 136 +++++++ .../formatters/EventFormatterFactory.java | 17 + .../JSONDataTreeCandidateFormatter.java | 89 +++++ .../formatters/JSONNotificationFormatter.java | 66 ++++ .../formatters/NotificationFormatter.java | 80 ++++ .../NotificationFormatterFactory.java | 19 + .../XMLDataTreeCandidateFormatter.java | 88 +++++ .../formatters/XMLNotificationFormatter.java | 81 ++++ .../AbstractWebsocketSerializer.java | 138 +++++++ .../JsonDataTreeCandidateSerializer.java | 76 ++++ .../XmlDataTreeCandidateSerializer.java | 71 ++++ restconf/restconf-nb-rfc8040/pom.xml | 11 + .../streams/listeners/ListenerAdapter.java | 364 +++--------------- .../NotificationListenerAdapter.java | 142 +++---- ...RestconfDocumentedExceptionMapperTest.java | 6 +- ...java => JsonNotificationListenerTest.java} | 17 +- .../listeners/ListenerAdapterTest.java | 181 ++++++++- .../XmlNotificationListenerTest.java | 239 ++++++++++++ .../listener-adapter-test/notif-create.json | 64 +-- .../listener-adapter-test/notif-create.xml | 17 + .../listener-adapter-test/notif-del.json | 34 +- .../listener-adapter-test/notif-delete.xml | 9 + .../notif-leaves-create.json | 36 +- .../notif-leaves-create.xml | 19 + .../notif-leaves-del.json | 8 +- .../notif-leaves-update.json | 36 +- .../notif-leaves-update.xml | 19 + .../listener-adapter-test/notif-update.json | 67 +--- .../listener-adapter-test/notif-update.xml | 18 + .../notif-without-data-create.json | 20 +- .../notif-without-data-create.xml | 9 + .../notif-without-data-del.json | 20 +- .../notif-without-data-delete.xml | 9 + .../notif-without-data-update.json | 20 +- .../notif-without-data-update.xml | 9 + 38 files changed, 1732 insertions(+), 619 deletions(-) create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatterFactory.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatterFactory.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONDataTreeCandidateFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONNotificationFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatterFactory.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLDataTreeCandidateFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLNotificationFormatter.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/AbstractWebsocketSerializer.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/JsonDataTreeCandidateSerializer.java create mode 100644 restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/XmlDataTreeCandidateSerializer.java rename restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/{NotificationListenerTest.java => JsonNotificationListenerTest.java} (94%) create mode 100644 restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/XmlNotificationListenerTest.java create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-delete.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-delete.xml create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.xml diff --git a/restconf/restconf-common/pom.xml b/restconf/restconf-common/pom.xml index 23cdff21b5..2893439a6e 100644 --- a/restconf/restconf-common/pom.xml +++ b/restconf/restconf-common/pom.xml @@ -29,6 +29,14 @@ org.opendaylight.yangtools yang-data-api + + org.opendaylight.yangtools + yang-data-codec-gson + + + org.opendaylight.yangtools + yang-data-codec-xml + org.opendaylight.yangtools yang-data-impl 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 index 0000000000..547c7bc389 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatter.java @@ -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> { + + protected DataTreeCandidateFormatter() { + } + + public DataTreeCandidateFormatter(String xpathFilter) throws XPathExpressionException { + super(xpathFilter); + } + + @Override + final void fillDocument(Document doc, EffectiveModelContext schemaContext, Collection 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 index 0000000000..d5b15fb4c5 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/DataTreeCandidateFormatterFactory.java @@ -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> { + + @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 index 0000000000..dc6482592a --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatter.java @@ -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 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 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 index 0000000000..b5868814f4 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/EventFormatterFactory.java @@ -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 { + + EventFormatter getFormatter(); + + EventFormatter 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 index 0000000000..38c585a62e --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONDataTreeCandidateFormatter.java @@ -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 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 index 0000000000..a0f5a442ce --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/JSONNotificationFormatter.java @@ -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 index 0000000000..49381fdb34 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatter.java @@ -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 { + 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 index 0000000000..9d30b9ea45 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/NotificationFormatterFactory.java @@ -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 { + @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 index 0000000000..404da42e2f --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLDataTreeCandidateFormatter.java @@ -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 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 index 0000000000..d2af552690 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/formatters/XMLNotificationFormatter.java @@ -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 index 0000000000..91880092b5 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/AbstractWebsocketSerializer.java @@ -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 { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractWebsocketSerializer.class); + + public void serialize(DataTreeCandidate candidate, boolean leafNodesOnly, boolean skipData) throws T { + final Deque path = new ArrayDeque<>(); + path.addAll(candidate.getRootPath().getPathArguments()); + if (leafNodesOnly) { + serializeLeafNodesOnly(path, candidate.getRootNode(), skipData); + return; + } + + serializeData(path, candidate.getRootNode(), skipData); + } + + void serializeLeafNodesOnly(Deque 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 path, DataTreeCandidateNode candidate, boolean skipData) + throws T; + + abstract void serializePath(Collection pathArguments) throws T; + + abstract void serializeOperation(DataTreeCandidateNode candidate) throws T; + + String convertPath(Collection 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> keys = + ((YangInstanceIdentifier.NodeIdentifierWithPredicates) pathArgument).entrySet(); + for (Map.Entry 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 index 0000000000..37fb4ad3b4 --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/JsonDataTreeCandidateSerializer.java @@ -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 { + + 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 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 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 index 0000000000..ea65fae94d --- /dev/null +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/serializer/XmlDataTreeCandidateSerializer.java @@ -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 { + + private final EffectiveModelContext context; + private final XMLStreamWriter xmlWriter; + + public XmlDataTreeCandidateSerializer(EffectiveModelContext context, XMLStreamWriter xmlWriter) { + this.context = context; + this.xmlWriter = xmlWriter; + } + + @Override + void serializeData(Collection 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 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(); + } +} diff --git a/restconf/restconf-nb-rfc8040/pom.xml b/restconf/restconf-nb-rfc8040/pom.xml index 62b6efcfb6..3a5d54ceda 100644 --- a/restconf/restconf-nb-rfc8040/pom.xml +++ b/restconf/restconf-nb-rfc8040/pom.xml @@ -201,6 +201,17 @@ threadpool-config-impl test + + org.xmlunit + xmlunit-assertj + test + + + net.javacrumbs.json-unit + json-unit-assertj + 2.18.1 + test + diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java index 655e48bc39..a237f17f73 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java @@ -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 dataTreeCandidates) { final Instant now = Instant.now(); if (!checkStartStop(now, this)) { return; } - final String xml = prepareXml(dataTreeCandidates); - if (checkFilter(xml)) { - prepareAndPostData(xml); + final Optional 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 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 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> 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> 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> 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 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 = 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) diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerAdapter.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerAdapter.java index c1cbdd98f8..9745083dc5 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerAdapter.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerAdapter.java @@ -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 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) diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapperTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapperTest.java index dd4ab693db..c555b92978 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapperTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapperTest.java @@ -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()); diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/JsonNotificationListenerTest.java similarity index 94% rename from restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerTest.java rename to restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/JsonNotificationListenerTest.java index a964815b67..093f11724a 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/NotificationListenerTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/JsonNotificationListenerTest.java @@ -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(); } } diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java index 0cbf42d7f1..7381680d4c 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java @@ -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(".*", "someDate"); + } + + 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 iid = InstanceIdentifier.create(PatchCont.class) + final InstanceIdentifier 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 iid = InstanceIdentifier.create(PatchCont.class) + final InstanceIdentifier 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 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 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 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 index 0000000000..1f2cf65970 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/XmlNotificationListenerTest.java @@ -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 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 = "" + + "2020-06-29T14:23:46.086855+02:00" + + "value"; + + 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 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 = "" + + "2020-06-29T14:23:46.086855+02:00" + + "value"; + + 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 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 = "" + + "2020-06-29T14:23:46.086855+02:00" + + "value"; + + 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 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 = "" + + "2020-06-29T14:23:46.086855+02:00" + + "value"; + + 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 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 = "" + + "2020-06-29T14:23:46.086855+02:00" + + "value"; + + 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 leaf) { + final AugmentationNode augm = mock(AugmentationNode.class); + final AugmentationIdentifier augmId = new AugmentationIdentifier(SingletonSet.of(leaf.getNodeType())); + when(augm.getIdentifier()).thenReturn(augmId); + + final Collection> childs = new ArrayList<>(); + childs.add(leaf); + + when(augm.getValue()).thenReturn(childs); + return augm; + } + + private static MapEntryNode mockMapEntry(final QName entryQName, final LeafNode 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> 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 child) { + final ContainerNode cont = mock(ContainerNode.class); + when(cont.getIdentifier()).thenReturn(NodeIdentifier.create(contQName)); + + final Collection> childs = new ArrayList<>(); + childs.add(child); + when(cont.getValue()).thenReturn(childs); + return cont; + } + + private static LeafNode mockLeaf(final QName leafQName) { + final LeafNode 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(); + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json index 31a7f1a412..39748b97d6 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json @@ -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 index 0000000000..6f6678af6f --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.xml @@ -0,0 +1,17 @@ + + 2020-10-19T12:21:02.876756+02:00 + + + /instance-identifier-patch-module:patch-cont + + + + Althea + Jed + + + + created + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json index a7cb21379d..0c79ab6412 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json @@ -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 index 0000000000..b66adc0b48 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-delete.xml @@ -0,0 +1,9 @@ + + 2020-10-19T12:21:02.876756+02:00 + + + /instance-identifier-patch-module:patch-cont + deleted + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json index f685b89605..c46115ef09 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json @@ -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 index 0000000000..b751d50884 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.xml @@ -0,0 +1,19 @@ + + 2020-10-19T13:50:24.917103+02:00 + + + /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 + + Jed + + created + + + /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 + + Althea + + created + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json index fd1f1d8e0d..b82d295f24 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json @@ -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", } } diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json index 890d945f7e..7a0b6161d3 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json @@ -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 index 0000000000..183dd27fa0 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.xml @@ -0,0 +1,19 @@ + + 2020-10-19T13:54:31.86969+02:00 + + + /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 + + Althea + + updated + + + /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 + + Bertha + + created + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json index 0ee0547f1b..55dc269df8 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json @@ -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 index 0000000000..b1f79531c7 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.xml @@ -0,0 +1,18 @@ + + 2020-10-19T13:41:50.132897+02:00 + + + /instance-identifier-patch-module:patch-cont + + + + Althea + Jed + Bertha + + + + updated + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json index 6e4dadc36e..5ba5012223 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.json @@ -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 index 0000000000..29e051aef0 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-create.xml @@ -0,0 +1,9 @@ + + 2020-10-19T12:21:02.876756+02:00 + + + /instance-identifier-patch-module:patch-cont + created + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json index dc3f739779..bcf15972a5 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-del.json @@ -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 index 0000000000..b66adc0b48 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-delete.xml @@ -0,0 +1,9 @@ + + 2020-10-19T12:21:02.876756+02:00 + + + /instance-identifier-patch-module:patch-cont + deleted + + + \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json index c22c956216..703b978f9b 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.json @@ -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 index 0000000000..51d8adae25 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-without-data-update.xml @@ -0,0 +1,9 @@ + + 2020-10-19T12:21:02.876756+02:00 + + + /instance-identifier-patch-module:patch-cont + updated + + + \ No newline at end of file -- 2.36.6