<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-data-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-codec-gson</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-codec-xml</artifactId>
+ </dependency>
<dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-data-impl</artifactId>
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import static org.opendaylight.restconf.common.formatters.NotificationFormatter.XML_OUTPUT_FACTORY;
+
+import com.google.common.annotations.Beta;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/**
+ * Base formatter for DataTreeCandidates which only handles exporting to a document for filter checking purpose.
+ */
+@Beta
+public abstract class DataTreeCandidateFormatter extends EventFormatter<Collection<DataTreeCandidate>> {
+
+ protected DataTreeCandidateFormatter() {
+ }
+
+ public DataTreeCandidateFormatter(String xpathFilter) throws XPathExpressionException {
+ super(xpathFilter);
+ }
+
+ @Override
+ final void fillDocument(Document doc, EffectiveModelContext schemaContext, Collection<DataTreeCandidate> input)
+ throws IOException {
+ final Element notificationElement = doc.createElementNS("urn:ietf:params:xml:ns:netconf:notification:1.0",
+ "notification");
+ final Element eventTimeElement = doc.createElement("eventTime");
+ eventTimeElement.setTextContent(toRFC3339(Instant.now()));
+ notificationElement.appendChild(eventTimeElement);
+
+ final Element notificationEventElement = doc.createElementNS(
+ "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
+
+ for (DataTreeCandidate candidate : input) {
+ final Element dataChangedElement = doc.createElement("data-changed-event");
+ try {
+ final Element dataElement = doc.createElement("data");
+ final DOMResult domResult = new DOMResult(dataElement);
+ final XMLStreamWriter writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(domResult);
+
+ final SchemaPath path = SchemaPath.create(candidate.getRootPath().getPathArguments().stream()
+ .filter(p -> !(p instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
+ .map(YangInstanceIdentifier.PathArgument::getNodeType).collect(Collectors.toList()), true);
+
+ writeCandidate(XMLStreamNormalizedNodeStreamWriter.create(writer, schemaContext,
+ path), candidate);
+
+ dataChangedElement.appendChild(dataElement);
+ } catch (final XMLStreamException e) {
+ throw new IOException("Failed to write notification content", e);
+ }
+ notificationElement.appendChild(notificationEventElement);
+ }
+ doc.appendChild(notificationElement);
+ }
+
+ static void writeCandidate(final NormalizedNodeStreamWriter writer, final DataTreeCandidate candidate)
+ throws IOException {
+ if (candidate.getRootNode().getDataAfter().isPresent()) {
+ try (NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(writer)) {
+ nodeWriter.write(candidate.getRootNode().getDataAfter().get());
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import java.util.Collection;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+
+public interface DataTreeCandidateFormatterFactory extends EventFormatterFactory<Collection<DataTreeCandidate>> {
+
+ @Override
+ DataTreeCandidateFormatter getFormatter();
+
+ @Override
+ DataTreeCandidateFormatter getFormatter(String xpathFilter) throws XPathExpressionException;
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.w3c.dom.Document;
+
+public abstract class EventFormatter<T> implements Immutable {
+ private static final XPathFactory XPF = XPathFactory.newInstance();
+
+ // FIXME: NETCONF-369: XPath operates without namespace context, therefore we need an namespace-unaware builder.
+ // Once it is fixed we can use UntrustedXML instead.
+ private static final @NonNull DocumentBuilderFactory DBF;
+
+ static {
+ final DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
+ f.setCoalescing(true);
+ f.setExpandEntityReferences(false);
+ f.setIgnoringElementContentWhitespace(true);
+ f.setIgnoringComments(true);
+ f.setXIncludeAware(false);
+ try {
+ f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ f.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ f.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ } catch (final ParserConfigurationException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ DBF = f;
+ }
+
+ private final XPathExpression filter;
+
+ EventFormatter() {
+ this.filter = null;
+ }
+
+ EventFormatter(final String xpathFilter) throws XPathExpressionException {
+ final XPath xpath;
+ synchronized (XPF) {
+ xpath = XPF.newXPath();
+ }
+ // FIXME: NETCONF-369: we need to bind the namespace context here and for that we need the SchemaContext
+ filter = xpath.compile(xpathFilter);
+ }
+
+ public final Optional<String> eventData(final EffectiveModelContext schemaContext, final T input, final Instant now,
+ boolean leafNodesOnly, boolean skipData)
+ throws Exception {
+ if (!filterMatches(schemaContext, input, now)) {
+ return Optional.empty();
+ }
+ return Optional.of(createText(schemaContext, input, now, leafNodesOnly, skipData));
+ }
+
+ /**
+ * Export the provided input into the provided document so we can verify whether a filter matches the content.
+ *
+ * @param doc the document to fill
+ * @param schemaContext context to use for the export
+ * @param input data to export
+ * @throws IOException if any IOException occurs during export to the document
+ */
+ abstract void fillDocument(Document doc, EffectiveModelContext schemaContext, T input) throws IOException;
+
+ /**
+ * Format the input data into string representation of the data provided.
+ *
+ * @param schemaContext context to use for the export
+ * @param input input data
+ * @param now time the event happened
+ * @param leafNodesOnly option to include only leaves in the result
+ * @param skipData option to skip data in the result, only paths would be included
+ * @return String representation of the formatted data
+ * @throws Exception if the underlying formatters fail to export the data to the requested format
+ */
+ abstract String createText(EffectiveModelContext schemaContext, T input, Instant now, boolean leafNodesOnly,
+ boolean skipData) throws Exception;
+
+ private boolean filterMatches(final EffectiveModelContext schemaContext, final T input, final Instant now)
+ throws IOException {
+ if (filter == null) {
+ return true;
+ }
+
+ final Document doc;
+ try {
+ doc = DBF.newDocumentBuilder().newDocument();
+ } catch (final ParserConfigurationException e) {
+ throw new IOException("Failed to create a new document", e);
+ }
+ fillDocument(doc, schemaContext, input);
+
+ final Boolean eval;
+ try {
+ eval = (Boolean) filter.evaluate(doc, XPathConstants.BOOLEAN);
+ } catch (final XPathExpressionException e) {
+ throw new IllegalStateException("Failed to evaluate expression " + filter, e);
+ }
+
+ return eval.booleanValue();
+ }
+
+ /**
+ * Formats data specified by RFC3339.
+ *
+ * @param now time stamp
+ * @return Data specified by RFC3339.
+ */
+ static String toRFC3339(final Instant now) {
+ return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(OffsetDateTime.ofInstant(now, ZoneId.systemDefault()));
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import javax.xml.xpath.XPathExpressionException;
+
+public interface EventFormatterFactory<T> {
+
+ EventFormatter<T> getFormatter();
+
+ EventFormatter<T> getFormatter(String xpathFilter) throws XPathExpressionException;
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.time.Instant;
+import java.util.Collection;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.restconf.common.serializer.JsonDataTreeCandidateSerializer;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class JSONDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
+ private static final Logger LOG = LoggerFactory.getLogger(JSONDataTreeCandidateFormatter.class);
+ public static final String SAL_REMOTE_NAMESPACE = "urn-opendaylight-params-xml-ns-yang-controller-md-sal-remote";
+ public static final String NETCONF_NOTIFICATION_NAMESPACE = "urn-ietf-params-xml-ns-netconf-notification-1.0";
+ private final JSONCodecFactorySupplier codecSupplier;
+
+ private JSONDataTreeCandidateFormatter(final JSONCodecFactorySupplier codecSupplier) {
+ this.codecSupplier = requireNonNull(codecSupplier);
+ }
+
+ private JSONDataTreeCandidateFormatter(final String xpathFilter, final JSONCodecFactorySupplier codecSupplier)
+ throws XPathExpressionException {
+ super(xpathFilter);
+ this.codecSupplier = requireNonNull(codecSupplier);
+ }
+
+ public static DataTreeCandidateFormatterFactory createFactory(
+ final JSONCodecFactorySupplier codecSupplier) {
+ requireNonNull(codecSupplier);
+ return new DataTreeCandidateFormatterFactory() {
+ @Override
+ public DataTreeCandidateFormatter getFormatter(final String xpathFilter)
+ throws XPathExpressionException {
+ return new JSONDataTreeCandidateFormatter(xpathFilter, codecSupplier);
+ }
+
+ @Override
+ public DataTreeCandidateFormatter getFormatter() {
+ return new JSONDataTreeCandidateFormatter(codecSupplier);
+ }
+ };
+ }
+
+ @Override
+ String createText(final EffectiveModelContext schemaContext, final Collection<DataTreeCandidate> input,
+ final Instant now, boolean leafNodesOnly, boolean skipData)
+ throws IOException {
+ final Writer writer = new StringWriter();
+ final JsonWriter jsonWriter = new JsonWriter(writer).beginObject();
+
+ jsonWriter.name(NETCONF_NOTIFICATION_NAMESPACE + ":notification").beginObject();
+ jsonWriter.name(SAL_REMOTE_NAMESPACE + ":data-changed-notification").beginObject();
+ jsonWriter.name("data-change-event").beginArray();
+
+ final JsonDataTreeCandidateSerializer serializer =
+ new JsonDataTreeCandidateSerializer(codecSupplier, schemaContext, jsonWriter);
+ for (final DataTreeCandidate candidate : input) {
+ serializer.serialize(candidate, leafNodesOnly, skipData);
+ }
+
+ // data-change-event
+ jsonWriter.endArray();
+ // data-changed-notification
+ jsonWriter.endObject();
+
+ jsonWriter.name("event-time").value(toRFC3339(now));
+ jsonWriter.endObject();
+
+ // notification
+ jsonWriter.endObject();
+
+ return writer.toString();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import java.io.IOException;
+import java.time.Instant;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.mdsal.dom.api.DOMEvent;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+public abstract class NotificationFormatter extends EventFormatter<DOMNotification> {
+ protected static final XMLOutputFactory XML_OUTPUT_FACTORY;
+
+ static {
+ XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory();
+ XML_OUTPUT_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
+ }
+
+ NotificationFormatter() {
+
+ }
+
+ public NotificationFormatter(final String xpathFilter) throws XPathExpressionException {
+ super(xpathFilter);
+ }
+
+ @Override
+ void fillDocument(Document doc, EffectiveModelContext schemaContext, DOMNotification input) throws IOException {
+ final Element notificationElement = doc.createElementNS("urn:ietf:params:xml:ns:netconf:notification:1.0",
+ "notification");
+ final Element eventTimeElement = doc.createElement("eventTime");
+ if (input instanceof DOMEvent) {
+ eventTimeElement.setTextContent(toRFC3339(((DOMEvent) input).getEventInstant()));
+ } else {
+ eventTimeElement.setTextContent(toRFC3339(Instant.now()));
+ }
+ notificationElement.appendChild(eventTimeElement);
+
+ final Element notificationEventElement = doc.createElementNS(
+ "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "create-notification-stream");
+ final Element dataElement = doc.createElement("notification");
+ final DOMResult result = new DOMResult(dataElement);
+ try {
+ final XMLStreamWriter writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(result);
+ try {
+ writeNotificationBody(XMLStreamNormalizedNodeStreamWriter.create(writer, schemaContext,
+ input.getType()), input.getBody());
+ } finally {
+ writer.close();
+ }
+ } catch (final XMLStreamException e) {
+ throw new IOException("Failed to write notification content", e);
+ }
+ notificationElement.appendChild(notificationEventElement);
+ doc.appendChild(notificationElement);
+ }
+
+ static void writeNotificationBody(final NormalizedNodeStreamWriter writer, final ContainerNode body)
+ throws IOException {
+ try (NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(writer)) {
+ nodeWriter.write(body);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+
+public interface NotificationFormatterFactory extends EventFormatterFactory<DOMNotification> {
+ @Override
+ NotificationFormatter getFormatter();
+
+ @Override
+ NotificationFormatter getFormatter(String xpathFilter) throws XPathExpressionException;
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.formatters;
+
+import static org.opendaylight.restconf.common.formatters.NotificationFormatter.XML_OUTPUT_FACTORY;
+import static org.opendaylight.restconf.common.formatters.XMLNotificationFormatter.NOTIFICATION_ELEMENT;
+import static org.opendaylight.restconf.common.formatters.XMLNotificationFormatter.NOTIFICATION_NAMESPACE;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.util.Collection;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.xpath.XPathExpressionException;
+import org.opendaylight.restconf.common.serializer.XmlDataTreeCandidateSerializer;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+public final class XMLDataTreeCandidateFormatter extends DataTreeCandidateFormatter {
+ private static final XMLDataTreeCandidateFormatter INSTANCE = new XMLDataTreeCandidateFormatter();
+
+ public static final DataTreeCandidateFormatterFactory FACTORY =
+ new DataTreeCandidateFormatterFactory() {
+ @Override
+ public XMLDataTreeCandidateFormatter getFormatter(final String xpathFilter)
+ throws XPathExpressionException {
+ return new XMLDataTreeCandidateFormatter(xpathFilter);
+ }
+
+ @Override
+ public XMLDataTreeCandidateFormatter getFormatter() {
+ return INSTANCE;
+ }
+ };
+
+ private XMLDataTreeCandidateFormatter() {
+ }
+
+ private XMLDataTreeCandidateFormatter(final String xpathFilter) throws XPathExpressionException {
+ super(xpathFilter);
+ }
+
+ @Override
+ String createText(EffectiveModelContext schemaContext, Collection<DataTreeCandidate> input, Instant now,
+ boolean leafNodesOnly, boolean skipData)
+ throws Exception {
+ StringWriter writer = new StringWriter();
+
+ final XMLStreamWriter xmlStreamWriter;
+ try {
+ xmlStreamWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(writer);
+ xmlStreamWriter.setDefaultNamespace(NOTIFICATION_NAMESPACE);
+
+ xmlStreamWriter.writeStartElement(NOTIFICATION_NAMESPACE, NOTIFICATION_ELEMENT);
+ xmlStreamWriter.writeDefaultNamespace(NOTIFICATION_NAMESPACE);
+
+ xmlStreamWriter.writeStartElement("eventTime");
+ xmlStreamWriter.writeCharacters(toRFC3339(now));
+ xmlStreamWriter.writeEndElement();
+
+ xmlStreamWriter.writeStartElement("data-changed-notification");
+
+ final XmlDataTreeCandidateSerializer serializer =
+ new XmlDataTreeCandidateSerializer(schemaContext, xmlStreamWriter);
+
+ for (final DataTreeCandidate candidate : input) {
+ serializer.serialize(candidate, leafNodesOnly, skipData);
+ }
+
+ // data-changed-notification
+ xmlStreamWriter.writeEndElement();
+
+ // notification
+ xmlStreamWriter.writeEndElement();
+ } catch (XMLStreamException e) {
+ throw new IOException("Failed to write notification content", e);
+ }
+
+
+ return writer.toString();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.serializer;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Set;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractWebsocketSerializer<T extends Exception> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractWebsocketSerializer.class);
+
+ public void serialize(DataTreeCandidate candidate, boolean leafNodesOnly, boolean skipData) throws T {
+ final Deque<PathArgument> path = new ArrayDeque<>();
+ path.addAll(candidate.getRootPath().getPathArguments());
+ if (leafNodesOnly) {
+ serializeLeafNodesOnly(path, candidate.getRootNode(), skipData);
+ return;
+ }
+
+ serializeData(path, candidate.getRootNode(), skipData);
+ }
+
+ void serializeLeafNodesOnly(Deque<PathArgument> path, DataTreeCandidateNode candidate, boolean skipData)
+ throws T {
+ NormalizedNode<?, ?> node = null;
+ switch (candidate.getModificationType()) {
+ case UNMODIFIED:
+ // no reason to do anything with an unmodified node
+ LOG.debug("DataTreeCandidate for a notification is unmodified, not serializing leaves. Candidate: {}",
+ candidate);
+ break;
+ case SUBTREE_MODIFIED:
+ case WRITE:
+ case APPEARED:
+ node = candidate.getDataAfter().get();
+ break;
+ case DELETE:
+ case DISAPPEARED:
+ node = candidate.getDataBefore().get();
+ break;
+ default:
+ LOG.error("DataTreeCandidate modification has unknown type: {}", candidate.getModificationType());
+ }
+
+ if (node == null) {
+ return;
+ }
+
+ if (node instanceof LeafNode<?> || node instanceof LeafSetNode) {
+ serializeData(path, candidate, skipData);
+ return;
+ }
+
+ for (DataTreeCandidateNode childNode : candidate.getChildNodes()) {
+ path.add(childNode.getIdentifier());
+ serializeLeafNodesOnly(path, childNode, skipData);
+ path.removeLast();
+ }
+ }
+
+ abstract void serializeData(Collection<PathArgument> path, DataTreeCandidateNode candidate, boolean skipData)
+ throws T;
+
+ abstract void serializePath(Collection<PathArgument> pathArguments) throws T;
+
+ abstract void serializeOperation(DataTreeCandidateNode candidate) throws T;
+
+ String convertPath(Collection<PathArgument> path) {
+ final StringBuilder pathBuilder = new StringBuilder();
+
+ for (PathArgument pathArgument : path) {
+ pathBuilder.append("/");
+ pathBuilder.append(pathArgument.getNodeType().getNamespace().toString().replaceAll(":", "-"));
+ pathBuilder.append(":");
+ pathBuilder.append(pathArgument.getNodeType().getLocalName());
+
+ if (pathArgument instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
+ pathBuilder.append("[");
+ final Set<Map.Entry<QName, Object>> keys =
+ ((YangInstanceIdentifier.NodeIdentifierWithPredicates) pathArgument).entrySet();
+ for (Map.Entry<QName, Object> key : keys) {
+ pathBuilder.append(key.getKey().getNamespace().toString().replaceAll(":", "-"));
+ pathBuilder.append(":");
+ pathBuilder.append(key.getKey().getLocalName());
+ pathBuilder.append("='");
+ pathBuilder.append(key.getValue().toString());
+ pathBuilder.append("'");
+ }
+ pathBuilder.append("]");
+ }
+ }
+
+ return pathBuilder.toString();
+ }
+
+ String modificationTypeToOperation(DataTreeCandidateNode candidate, ModificationType modificationType) {
+ switch (modificationType) {
+ case UNMODIFIED:
+ // shouldn't ever happen since the root of a modification is only triggered by some event
+ LOG.warn("DataTreeCandidate for a notification is unmodified. Candidate: {}", candidate);
+ return "none";
+ case SUBTREE_MODIFIED:
+ case WRITE:
+ case APPEARED:
+ if (candidate.getDataBefore().isPresent()) {
+ return "updated";
+ } else {
+ return "created";
+ }
+ case DELETE:
+ case DISAPPEARED:
+ return "deleted";
+ default:
+ LOG.error("DataTreeCandidate modification has unknown type: {}",
+ candidate.getModificationType());
+ throw new IllegalStateException("Unknown modification type");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.serializer;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+public class JsonDataTreeCandidateSerializer extends AbstractWebsocketSerializer<IOException> {
+
+ private final JSONCodecFactorySupplier codecSupplier;
+ private final EffectiveModelContext context;
+ private final JsonWriter jsonWriter;
+
+ public JsonDataTreeCandidateSerializer(final JSONCodecFactorySupplier codecSupplier,
+ final EffectiveModelContext context,
+ final JsonWriter jsonWriter) {
+
+ this.codecSupplier = codecSupplier;
+ this.context = context;
+ this.jsonWriter = jsonWriter;
+ }
+
+ void serializeData(Collection<YangInstanceIdentifier.PathArgument> nodePath, DataTreeCandidateNode candidate,
+ boolean skipData)
+ throws IOException {
+ final SchemaPath path = SchemaPath.create(nodePath.stream()
+ .filter(p -> !(p instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
+ .map(YangInstanceIdentifier.PathArgument::getNodeType).collect(Collectors.toList()), true);
+ final NormalizedNodeStreamWriter nestedWriter =
+ JSONNormalizedNodeStreamWriter
+ .createNestedWriter(codecSupplier.getShared(context), path.getParent(), null, jsonWriter);
+
+ jsonWriter.beginObject();
+ serializePath(nodePath);
+
+ if (!skipData && candidate.getDataAfter().isPresent()) {
+ jsonWriter.name("data").beginObject();
+ NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(nestedWriter);
+ nodeWriter.write(candidate.getDataAfter().get());
+ nodeWriter.flush();
+
+ // end data
+ jsonWriter.endObject();
+ }
+
+ serializeOperation(candidate);
+ jsonWriter.endObject();
+ }
+
+ @Override
+ void serializeOperation(final DataTreeCandidateNode candidate)
+ throws IOException {
+ jsonWriter.name("operation").value(modificationTypeToOperation(candidate, candidate.getModificationType()));
+ }
+
+ @Override
+ void serializePath(Collection<YangInstanceIdentifier.PathArgument> pathArguments)
+ throws IOException {
+ jsonWriter.name("path").value(convertPath(pathArguments));
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.common.serializer;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+public class XmlDataTreeCandidateSerializer extends AbstractWebsocketSerializer<Exception> {
+
+ private final EffectiveModelContext context;
+ private final XMLStreamWriter xmlWriter;
+
+ public XmlDataTreeCandidateSerializer(EffectiveModelContext context, XMLStreamWriter xmlWriter) {
+ this.context = context;
+ this.xmlWriter = xmlWriter;
+ }
+
+ @Override
+ void serializeData(Collection<PathArgument> nodePath, DataTreeCandidateNode candidate, boolean skipData)
+ throws Exception {
+ final SchemaPath path = SchemaPath.create(nodePath.stream()
+ .filter(p -> !(p instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
+ .map(PathArgument::getNodeType).collect(Collectors.toList()), true);
+ final NormalizedNodeStreamWriter nodeStreamWriter =
+ XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, context, path.getParent());
+
+ xmlWriter.writeStartElement("data-changed-event");
+ serializePath(nodePath);
+
+ if (!skipData && candidate.getDataAfter().isPresent()) {
+ xmlWriter.writeStartElement("data");
+ NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(nodeStreamWriter);
+ nnWriter.write(candidate.getDataAfter().get());
+ nnWriter.flush();
+
+ xmlWriter.writeEndElement();
+ }
+ serializeOperation(candidate);
+
+ xmlWriter.writeEndElement();
+ }
+
+ @Override
+ public void serializePath(Collection<PathArgument> pathArguments) throws XMLStreamException {
+ xmlWriter.writeStartElement("path");
+ xmlWriter.writeCharacters(convertPath(pathArguments));
+ xmlWriter.writeEndElement();
+ }
+
+ @Override
+ public void serializeOperation(DataTreeCandidateNode candidate) throws XMLStreamException {
+ xmlWriter.writeStartElement("operation");
+ xmlWriter.writeCharacters(modificationTypeToOperation(candidate, candidate.getModificationType()));
+ xmlWriter.writeEndElement();
+ }
+}
<artifactId>threadpool-config-impl</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.xmlunit</groupId>
+ <artifactId>xmlunit-assertj</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.javacrumbs.json-unit</groupId>
+ <artifactId>json-unit-assertj</artifactId>
+ <version>2.18.1</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
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.
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.
*
this.path = requireNonNull(path);
this.streamName = requireNonNull(streamName);
checkArgument(!streamName.isEmpty());
+
+ formatter = getFormatterFactory().getFormatter();
+ }
+
+ private DataTreeCandidateFormatterFactory getFormatterFactory() {
+ switch (outputType) {
+ case JSON:
+ return JSON_FORMATTER_FACTORY;
+ case XML:
+ return XMLDataTreeCandidateFormatter.FACTORY;
+ default:
+ throw new IllegalArgumentException(("Unsupported outputType" + outputType));
+ }
+ }
+
+ private DataTreeCandidateFormatter getFormatter(final String filter) throws XPathExpressionException {
+ final DataTreeCandidateFormatterFactory factory = getFormatterFactory();
+ return filter == null || filter.isEmpty() ? factory.getFormatter() : factory.getFormatter(filter);
}
@Override
+ public void setQueryParams(final Instant start, final Instant stop, final String filter,
+ final boolean leafNodesOnly, final boolean skipNotificationData) {
+ super.setQueryParams(start, stop, filter, leafNodesOnly, skipNotificationData);
+ try {
+ this.formatter = getFormatter(filter);
+ } catch (final XPathExpressionException e) {
+ throw new IllegalArgumentException("Failed to get filter", e);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("checkstyle:IllegalCatch")
public void onDataTreeChanged(final Collection<DataTreeCandidate> dataTreeCandidates) {
final Instant now = Instant.now();
if (!checkStartStop(now, this)) {
return;
}
- final String xml = prepareXml(dataTreeCandidates);
- if (checkFilter(xml)) {
- prepareAndPostData(xml);
+ final Optional<String> maybeData;
+ try {
+ maybeData = formatter.eventData(schemaHandler.get(), dataTreeCandidates, now, getLeafNodesOnly(),
+ isSkipNotificationData());
+ } catch (final Exception e) {
+ LOG.error("Failed to process notification {}",
+ dataTreeCandidates.stream().map(Object::toString).collect(Collectors.joining(",")), e);
+ return;
+ }
+
+ if (maybeData.isPresent()) {
+ post(maybeData.get());
}
}
return this.path;
}
- /**
- * Prepare data of notification and data to client.
- *
- * @param xml XML-formatted data.
- */
- private void prepareAndPostData(final String xml) {
- if (this.outputType.equals(NotificationOutputType.JSON)) {
- post(XML.toJSONObject(xml).toString());
- } else {
- post(xml);
- }
- }
-
- /**
- * Prepare data in printable form and transform it to String.
- *
- * @param dataTreeCandidates Data-tree candidates to be transformed.
- * @return Data in printable form.
- */
- private String prepareXml(final Collection<DataTreeCandidate> dataTreeCandidates) {
- final EffectiveModelContext schemaContext = schemaHandler.get();
- final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext);
- final Document doc = createDocument();
- final Element notificationElement = basePartDoc(doc);
-
- final Element dataChangedNotificationEventElement = doc.createElementNS(
- "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
-
- addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, dataTreeCandidates,
- schemaContext, dataContextTree);
- notificationElement.appendChild(dataChangedNotificationEventElement);
- return transformDoc(doc);
- }
-
- /**
- * Adds values to data changed notification event element.
- */
- private void addValuesToDataChangedNotificationEventElement(final Document doc,
- final Element dataChangedNotificationEventElement, final Collection<DataTreeCandidate> dataTreeCandidates,
- final EffectiveModelContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
-
- for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) {
- DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode();
- if (candidateNode == null) {
- continue;
- }
- YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
- boolean isSkipNotificationData = this.isSkipNotificationData();
- if (isSkipNotificationData) {
- createCreatedChangedDataChangeEventElementWithoutData(doc, dataChangedNotificationEventElement,
- dataTreeCandidate.getRootNode(), schemaContext);
- } else {
- addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
- yiid.getParent(), schemaContext, dataSchemaContextTree);
- }
- }
- }
-
- private void addNodeToDataChangeNotificationEventElement(final Document doc,
- final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
- final YangInstanceIdentifier parentYiid, final EffectiveModelContext schemaContext,
- final DataSchemaContextTree dataSchemaContextTree) {
-
- Optional<NormalizedNode<?, ?>> optionalNormalizedNode = Optional.empty();
- switch (candidateNode.getModificationType()) {
- case APPEARED:
- case SUBTREE_MODIFIED:
- case WRITE:
- optionalNormalizedNode = candidateNode.getDataAfter();
- break;
- case DELETE:
- case DISAPPEARED:
- optionalNormalizedNode = candidateNode.getDataBefore();
- break;
- case UNMODIFIED:
- default:
- break;
- }
-
- if (!optionalNormalizedNode.isPresent()) {
- LOG.error("No node present in notification for {}", candidateNode);
- return;
- }
-
- NormalizedNode<?, ?> normalizedNode = optionalNormalizedNode.get();
- YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid)
- .append(normalizedNode.getIdentifier()).build();
-
- final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(yiid);
- checkState(childrenSchemaNode.isPresent());
- boolean isNodeMixin = childrenSchemaNode.get().isMixin();
- boolean isSkippedNonLeaf = getLeafNodesOnly() && !(normalizedNode instanceof LeafNode);
- if (!isNodeMixin && !isSkippedNonLeaf) {
- Node node = null;
- switch (candidateNode.getModificationType()) {
- case APPEARED:
- case SUBTREE_MODIFIED:
- case WRITE:
- Operation op = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
- node = createCreatedChangedDataChangeEventElement(doc, yiid, normalizedNode, op,
- schemaContext, dataSchemaContextTree);
- break;
- case DELETE:
- case DISAPPEARED:
- node = createDataChangeEventElement(doc, yiid, schemaContext, Operation.DELETED);
- break;
- case UNMODIFIED:
- default:
- break;
- }
- if (node != null) {
- dataChangedNotificationEventElement.appendChild(node);
- }
- }
-
- for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) {
- addNodeToDataChangeNotificationEventElement(
- doc, dataChangedNotificationEventElement, childNode, yiid, schemaContext, dataSchemaContextTree);
- }
- }
-
- /**
- * Creates data-changed event element from data.
- *
- * @param doc {@link Document}
- * @param schemaContext Schema context.
- * @param operation Operation value
- * @return {@link Node} represented by changed event element.
- */
- private static Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
- final SchemaContext schemaContext, Operation operation) {
- final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
- final Element pathElement = doc.createElement(PATH);
- addPathAsValueToElement(eventPath, pathElement, schemaContext);
- dataChangeEventElement.appendChild(pathElement);
-
- final Element operationElement = doc.createElement(OPERATION);
- operationElement.setTextContent(operation.value);
- dataChangeEventElement.appendChild(operationElement);
-
- return dataChangeEventElement;
- }
-
- /**
- * Creates data change notification element without data element.
- *
- * @param doc
- * {@link Document}
- * @param dataChangedNotificationEventElement
- * {@link Element}
- * @param candidateNode
- * {@link DataTreeCandidateNode}
- */
- private void createCreatedChangedDataChangeEventElementWithoutData(final Document doc,
- final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
- final SchemaContext schemaContext) {
- final Operation operation;
- switch (candidateNode.getModificationType()) {
- case APPEARED:
- case SUBTREE_MODIFIED:
- case WRITE:
- operation = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
- break;
- case DELETE:
- case DISAPPEARED:
- operation = Operation.DELETED;
- break;
- case UNMODIFIED:
- default:
- return;
- }
- Node dataChangeEventElement = createDataChangeEventElement(doc, getPath(), schemaContext, operation);
- dataChangedNotificationEventElement.appendChild(dataChangeEventElement);
- }
-
- private Node createCreatedChangedDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
- final NormalizedNode<?, ?> normalized, final Operation operation, final EffectiveModelContext schemaContext,
- final DataSchemaContextTree dataSchemaContextTree) {
- final Element dataChangeEventElement = doc.createElement(DATA_CHANGE_EVENT);
- final Element pathElement = doc.createElement(PATH);
- addPathAsValueToElement(eventPath, pathElement, schemaContext);
- dataChangeEventElement.appendChild(pathElement);
-
- final Element operationElement = doc.createElement("operation");
- operationElement.setTextContent(operation.value);
- dataChangeEventElement.appendChild(operationElement);
-
- try {
- final SchemaPath nodePath;
- final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(eventPath);
- checkState(childrenSchemaNode.isPresent());
- if (normalized instanceof MapEntryNode || normalized instanceof UnkeyedListEntryNode) {
- nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath();
- } else {
- nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath().getParent();
- }
- final DOMResult domResult = writeNormalizedNode(normalized, schemaContext, nodePath);
- final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
- final Element dataElement = doc.createElement("data");
- dataElement.appendChild(result);
- dataChangeEventElement.appendChild(dataElement);
- } catch (final IOException e) {
- LOG.error("Error in writer ", e);
- } catch (final XMLStreamException e) {
- LOG.error("Error processing stream", e);
- }
-
- return dataChangeEventElement;
- }
-
- /**
- * Adds path as value to element.
- *
- * @param eventPath Path to data in data store.
- * @param element {@link Element}
- * @param schemaContext Schema context.
- */
- private static void addPathAsValueToElement(final YangInstanceIdentifier eventPath, final Element element,
- final SchemaContext schemaContext) {
- final StringBuilder textContent = new StringBuilder();
-
- for (final PathArgument pathArgument : eventPath.getPathArguments()) {
- if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
- continue;
- }
- textContent.append("/");
- writeIdentifierWithNamespacePrefix(textContent, pathArgument.getNodeType(), schemaContext);
- if (pathArgument instanceof NodeIdentifierWithPredicates) {
- for (final Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) pathArgument).entrySet()) {
- final QName keyValue = entry.getKey();
- final String predicateValue = String.valueOf(entry.getValue());
- textContent.append("[");
- writeIdentifierWithNamespacePrefix(textContent, keyValue, schemaContext);
- textContent.append("='").append(predicateValue).append("']");
- }
- } else if (pathArgument instanceof NodeWithValue) {
- textContent.append("[.='").append(((NodeWithValue<?>) pathArgument).getValue()).append("']");
- }
- }
- element.setTextContent(textContent.toString());
- }
-
- /**
- * Writes identifier that consists of prefix and QName.
- *
- * @param textContent Text builder that should be supplemented by QName and its modules name.
- * @param qualifiedName QName of the element.
- * @param schemaContext Schema context that holds modules which should contain module specified in QName.
- */
- private static void writeIdentifierWithNamespacePrefix(final StringBuilder textContent, final QName qualifiedName,
- final SchemaContext schemaContext) {
- final Optional<Module> module = schemaContext.findModule(qualifiedName.getModule());
- if (module.isPresent()) {
- textContent.append(module.get().getName());
- textContent.append(":");
- textContent.append(qualifiedName.getLocalName());
- } else {
- LOG.error("Cannot write identifier with namespace prefix in data-change listener adapter: "
- + "Cannot find module in schema context for input QName {}.", qualifiedName);
- throw new IllegalStateException(String.format("Cannot find module in schema context for input QName %s.",
- qualifiedName));
- }
- }
-
- /**
- * Consists of three types {@link Operation#CREATED}, {@link Operation#UPDATED} and {@link Operation#DELETED}.
- */
- private enum Operation {
- CREATED("created"),
- UPDATED("updated"),
- DELETED("deleted");
-
- private final String value;
-
- Operation(final String value) {
- this.value = value;
- }
- }
-
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
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.
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.
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);
+ }
}
/**
*/
@Override
public String getOutputType() {
- return this.outputType;
+ return this.outputType.getName();
}
@Override
+ @SuppressWarnings("checkstyle:IllegalCatch")
public void onNotification(final DOMNotification notification) {
final Instant now = Instant.now();
if (!checkStartStop(now, this)) {
return;
}
- final EffectiveModelContext schemaContext = schemaHandler.get();
- final String xml = prepareXml(schemaContext, notification);
- if (checkFilter(xml)) {
- post(outputType.equals("JSON") ? prepareJson(schemaContext, notification) : xml);
+ final Optional<String> maybeOutput;
+ try {
+ maybeOutput = formatter.eventData(schemaHandler.get(), notification, now, getLeafNodesOnly(),
+ isSkipNotificationData());
+ } catch (Exception e) {
+ LOG.error("Failed to process notification {}", notification, e);
+ return;
+ }
+ if (maybeOutput.isPresent()) {
+ post(maybeOutput.get());
}
}
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)
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;
public Response expectedResponse;
@Test
- public void testMappingOfExceptionToResponse() {
+ public void testMappingOfExceptionToResponse() throws JSONException {
exceptionMapper.setHttpHeaders(httpHeaders);
final Response response = exceptionMapper.toResponse(thrownException);
compareResponseWithExpectation(expectedResponse, response);
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());
*/
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;
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.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;
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"));
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();
}
}
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;
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;
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);
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));
notificationLatch.countDown();
}
- public void assertGot(final String json) {
- if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 5, TimeUnit.SECONDS)) {
+ public void assertGot(final String json) throws JSONException {
+ if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 500, TimeUnit.SECONDS)) {
fail("Timed out waiting for notification for: " + json);
}
LOG.info("lastNotification: {}", lastNotification);
- String withFakeDate = withFakeDate(lastNotification);
+ final String withFakeDate = withFakeDate(lastNotification);
LOG.info("Comparing: \n{}\n{}", json, withFakeDate);
JSONAssert.assertEquals(json, withFakeDate, false);
this.lastNotification = null;
notificationLatch = new CountDownLatch(1);
}
+
+ public void assertXmlSimilar(String xml) {
+ awaitUntillNotification(xml);
+
+ LOG.info("lastNotification: {}", lastNotification);
+ final String withFakeDate = withFakeXmlDate(lastNotification);
+ LOG.info("Comparing: \n{}\n{}", xml, withFakeDate);
+
+ XmlAssert.assertThat(xml).and(withFakeDate).ignoreWhitespace().ignoreChildNodesOrder().areSimilar();
+ this.lastNotification = null;
+ notificationLatch = new CountDownLatch(1);
+ }
+
+ public String awaitUntillNotification(String xml) {
+ if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 500, TimeUnit.SECONDS)) {
+ fail("Timed out waiting for notification for: " + xml);
+ }
+ return lastNotification;
+ }
+
+ public void resetLatch() {
+ notificationLatch = new CountDownLatch(1);
+ }
}
- static String withFakeDate(final String in) {
- JSONObject doc = new JSONObject(in);
- JSONObject notification = doc.getJSONObject("notification");
+ static String withFakeDate(final String in) throws JSONException {
+ final JSONObject doc = new JSONObject(in);
+ final JSONObject notification =
+ doc.getJSONObject("urn-ietf-params-xml-ns-netconf-notification-1.0:notification");
if (notification == null) {
return in;
}
- notification.put("eventTime", "someDate");
+ notification.put("event-time", "someDate");
return doc.toString();
}
- private String getNotifJson(final String path) throws IOException, URISyntaxException {
- URL url = getClass().getResource(path);
- byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
+ static String withFakeXmlDate(String in) {
+ return in.replaceAll("<eventTime>.*</eventTime>", "<eventTime>someDate</eventTime>");
+ }
+
+ private String getNotifJson(final String path) throws IOException, URISyntaxException, JSONException {
+ final URL url = getClass().getResource(path);
+ final byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
return withFakeDate(new String(bytes, StandardCharsets.UTF_8));
}
+ private String getResultXml(final String path) throws IOException, URISyntaxException, JSONException {
+ final URL url = getClass().getResource(path);
+ final byte[] bytes = Files.readAllBytes(Paths.get(url.toURI()));
+ return withFakeXmlDate(new String(bytes, StandardCharsets.UTF_8));
+ }
+
@Test
public void testJsonNotifsLeaves() throws Exception {
ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
NotificationOutputTypeGrouping.NotificationOutputType.JSON, true, false);
adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
- DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+ final DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
.getInstance(DOMDataTreeChangeService.class);
- DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+ final DOMDataTreeIdentifier root =
+ new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
changeService.registerDataTreeChangeListener(root, adapter);
WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
- InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+ final InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
.child(MyList1.class, new MyList1Key("Althea"));
writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
writeTransaction.commit();
NotificationOutputTypeGrouping.NotificationOutputType.JSON, false, false);
adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
- DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+ final DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
.getInstance(DOMDataTreeChangeService.class);
- DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+ final DOMDataTreeIdentifier root =
+ new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
changeService.registerDataTreeChangeListener(root, adapter);
WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
- InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+ final InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
.child(MyList1.class, new MyList1Key("Althea"));
writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
writeTransaction.commit();
writeTransaction.commit();
adapter.assertGot(getNotifJson(JSON_NOTIF_WITHOUT_DATA_DELETE));
}
+
+ @Test
+ public void testXmlNotifications() throws Exception {
+ ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+ NotificationOutputTypeGrouping.NotificationOutputType.XML, false, false);
+ adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+ DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+ .getInstance(DOMDataTreeChangeService.class);
+ DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+ changeService.registerDataTreeChangeListener(root, adapter);
+ WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+ MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+ InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+ .child(MyList1.class, new MyList1Key("Althea"));
+ writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_CREATE));
+
+ writeTransaction = dataBroker.newWriteOnlyTransaction();
+ builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+ writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_UPDATE));
+
+ writeTransaction = dataBroker.newWriteOnlyTransaction();
+ writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_DEL));
+ }
+
+ @Test
+ public void testXmlSkipData() throws Exception {
+ ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+ NotificationOutputTypeGrouping.NotificationOutputType.XML, false, true);
+ adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+ DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+ .getInstance(DOMDataTreeChangeService.class);
+ DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+ changeService.registerDataTreeChangeListener(root, adapter);
+ WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+ MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+ InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+ .child(MyList1.class, new MyList1Key("Althea"));
+ writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_WITHOUT_DATA_CREATE));
+
+ writeTransaction = dataBroker.newWriteOnlyTransaction();
+ builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+ writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_WITHOUT_DATA_UPDATE));
+
+ writeTransaction = dataBroker.newWriteOnlyTransaction();
+ writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_WITHOUT_DATA_DELETE));
+ }
+
+ @Test
+ public void testXmlLeavesOnly() throws Exception {
+ ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey",
+ NotificationOutputTypeGrouping.NotificationOutputType.XML, true, false);
+ adapter.setCloseVars(transactionChainHandler, schemaContextHandler);
+
+ DOMDataTreeChangeService changeService = domDataBroker.getExtensions()
+ .getInstance(DOMDataTreeChangeService.class);
+ DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID);
+ changeService.registerDataTreeChangeListener(root, adapter);
+ WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction();
+ MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea");
+ InstanceIdentifier<MyList1> iid = InstanceIdentifier.create(PatchCont.class)
+ .child(MyList1.class, new MyList1Key("Althea"));
+ writeTransaction.mergeParentStructurePut(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_LEAVES_CREATE));
+
+ writeTransaction = dataBroker.newWriteOnlyTransaction();
+ builder = new MyList1Builder().withKey(new MyList1Key("Althea")).setMyLeaf12("Bertha");
+ writeTransaction.mergeParentStructureMerge(LogicalDatastoreType.CONFIGURATION, iid, builder.build());
+ writeTransaction.commit();
+ adapter.assertXmlSimilar(getResultXml(XML_NOTIF_LEAVES_UPDATE));
+
+ writeTransaction = dataBroker.newWriteOnlyTransaction();
+ writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid);
+ writeTransaction.commit();
+
+ // xmlunit cannot compare deeper children it seems out of the box so just check the iid encoding
+ final String notification = adapter.awaitUntillNotification("");
+ assertTrue(notification.contains("instance-identifier-patch-module:my-leaf12"));
+ assertTrue(notification.contains("instance-identifier-patch-module:my-leaf11"));
+ assertTrue(notification.contains("instance-identifier-patch-module:name"));
+ }
}
--- /dev/null
+/*
+ * Copyright (c) 2020 Pantheon.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import static org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+
+import com.google.common.collect.Lists;
+import java.net.URI;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.dom.api.DOMNotification;
+import org.opendaylight.restconf.nb.rfc8040.TestUtils;
+import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping;
+import org.opendaylight.yangtools.util.SingletonSet;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.xmlunit.assertj.XmlAssert;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class XmlNotificationListenerTest {
+ private static final QNameModule MODULE = QNameModule.create(URI.create("notifi:mod"), Revision.of("2016-11-23"));
+
+ private static EffectiveModelContext SCHEMA_CONTEXT;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ SCHEMA_CONTEXT = TestUtils.loadSchemaContext("/notifications");
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ SCHEMA_CONTEXT = null;
+ }
+
+ @Test
+ public void notifi_leafTest() throws Exception {
+ final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-leaf"));
+
+ final DOMNotification notificationData = mock(DOMNotification.class);
+
+ final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+ final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), leaf);
+
+ when(notificationData.getType()).thenReturn(schemaPathNotifi);
+ when(notificationData.getBody()).thenReturn(notifiBody);
+
+ final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+ final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+ + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-leaf xmlns=\"notifi:mod\">"
+ + "<lf>value</lf></notifi-leaf></notification>";
+
+ assertXmlMatches(result, control);
+ }
+
+ @Test
+ public void notifi_cont_leafTest() throws Exception {
+ final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-cont"));
+
+ final DOMNotification notificationData = mock(DOMNotification.class);
+
+ final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+ final ContainerNode cont = mockCont(QName.create(MODULE, "cont"), leaf);
+ final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), cont);
+
+ when(notificationData.getType()).thenReturn(schemaPathNotifi);
+ when(notificationData.getBody()).thenReturn(notifiBody);
+
+ final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+ final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+ + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-cont xmlns=\"notifi:mod\">"
+ + "<cont><lf>value</lf></cont></notifi-cont></notification>";
+
+ assertXmlMatches(result, control);
+ }
+
+ @Test
+ public void notifi_list_Test() throws Exception {
+ final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-list"));
+
+ final DOMNotification notificationData = mock(DOMNotification.class);
+
+ final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+ final MapEntryNode entry = mockMapEntry(QName.create(MODULE, "lst"), leaf);
+ final MapNode list = mockList(QName.create(MODULE, "lst"), entry);
+ final ContainerNode cont = mockCont(QName.create(MODULE, "cont"), list);
+ final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), cont);
+
+ when(notificationData.getType()).thenReturn(schemaPathNotifi);
+ when(notificationData.getBody()).thenReturn(notifiBody);
+
+ final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+ final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+ + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-list xmlns=\"notifi:mod\">"
+ + "<notifi-list><lf><lf>value</lf></lf></notifi-list></notifi-list></notification>";
+
+ assertXmlMatches(result, control);
+ }
+
+ @Test
+ public void notifi_grpTest() throws Exception {
+ final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-grp"));
+
+ final DOMNotification notificationData = mock(DOMNotification.class);
+
+ final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf"));
+ final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), leaf);
+
+ when(notificationData.getType()).thenReturn(schemaPathNotifi);
+ when(notificationData.getBody()).thenReturn(notifiBody);
+
+ final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+ final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+ + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-grp xmlns=\"notifi:mod\">"
+ + "<lf>value</lf></notifi-grp></notification>";
+
+ assertXmlMatches(result, control);
+ }
+
+ @Test
+ public void notifi_augmTest() throws Exception {
+ final Absolute schemaPathNotifi = Absolute.of(QName.create(MODULE, "notifi-augm"));
+
+ final DOMNotification notificationData = mock(DOMNotification.class);
+
+ final LeafNode<String> leaf = mockLeaf(QName.create(MODULE, "lf-augm"));
+ final AugmentationNode augm = mockAugm(leaf);
+ final ContainerNode notifiBody = mockCont(schemaPathNotifi.lastNodeIdentifier(), augm);
+
+ when(notificationData.getType()).thenReturn(schemaPathNotifi);
+ when(notificationData.getBody()).thenReturn(notifiBody);
+
+ final String result = prepareXmlResult(notificationData, schemaPathNotifi);
+
+ final String control = "<notification xmlns=\"urn:ietf:params:xml:ns:netconf:notification:1.0\">"
+ + "<eventTime>2020-06-29T14:23:46.086855+02:00</eventTime><notifi-augm xmlns=\"notifi:mod\">"
+ + "<lf-augm>value</lf-augm></notifi-augm></notification>";
+
+ assertXmlMatches(result, control);
+ }
+
+ private static void assertXmlMatches(String result, String control) {
+ XmlAssert.assertThat(result).and(control)
+ // text values have localName null but we want to compare those, ignore only nodes that have localName
+ // with eventTime value
+ .withNodeFilter(node -> node.getLocalName() == null || !node.getLocalName().equals("eventTime"))
+ .areSimilar();
+ }
+
+ private static AugmentationNode mockAugm(final LeafNode<String> leaf) {
+ final AugmentationNode augm = mock(AugmentationNode.class);
+ final AugmentationIdentifier augmId = new AugmentationIdentifier(SingletonSet.of(leaf.getNodeType()));
+ when(augm.getIdentifier()).thenReturn(augmId);
+
+ final Collection<DataContainerChild<? extends PathArgument, ?>> childs = new ArrayList<>();
+ childs.add(leaf);
+
+ when(augm.getValue()).thenReturn(childs);
+ return augm;
+ }
+
+ private static MapEntryNode mockMapEntry(final QName entryQName, final LeafNode<String> leaf) {
+ final MapEntryNode entry = mock(MapEntryNode.class);
+ final NodeIdentifierWithPredicates nodeId = NodeIdentifierWithPredicates.of(leaf.getNodeType(),
+ leaf.getNodeType(), "value");
+ when(entry.getIdentifier()).thenReturn(nodeId);
+ when(entry.getChild(any())).thenReturn(Optional.of(leaf));
+
+ final Collection<DataContainerChild<? extends PathArgument, ?>> childs = new ArrayList<>();
+ childs.add(leaf);
+
+ when(entry.getValue()).thenReturn(childs);
+ return entry;
+ }
+
+ private static MapNode mockList(final QName listQName, final MapEntryNode... entries) {
+ final MapNode list = mock(MapNode.class);
+ when(list.getIdentifier()).thenReturn(NodeIdentifier.create(listQName));
+ when(list.getValue()).thenReturn(Lists.newArrayList(entries));
+ return list;
+ }
+
+ private static ContainerNode mockCont(final QName contQName,
+ final DataContainerChild<? extends PathArgument, ?> child) {
+ final ContainerNode cont = mock(ContainerNode.class);
+ when(cont.getIdentifier()).thenReturn(NodeIdentifier.create(contQName));
+
+ final Collection<DataContainerChild<? extends PathArgument, ?>> childs = new ArrayList<>();
+ childs.add(child);
+ when(cont.getValue()).thenReturn(childs);
+ return cont;
+ }
+
+ private static LeafNode<String> mockLeaf(final QName leafQName) {
+ final LeafNode<String> child = mock(LeafNode.class);
+ when(child.getNodeType()).thenReturn(leafQName);
+ when(child.getIdentifier()).thenReturn(NodeIdentifier.create(leafQName));
+ when(child.getValue()).thenReturn("value");
+ return child;
+ }
+
+ private static String prepareXmlResult(final DOMNotification notificationData, final Absolute schemaPathNotifi)
+ throws Exception {
+ final NotificationListenerAdapter notifiAdapter = ListenersBroker.getInstance().registerNotificationListener(
+ schemaPathNotifi, "xml-stream", NotificationOutputTypeGrouping.NotificationOutputType.XML);
+ return notifiAdapter.formatter.eventData(SCHEMA_CONTEXT, notificationData, Instant.now(), false, false).get();
+ }
+}
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont</path>
+ <data>
+ <patch-cont xmlns="instance:identifier:patch:module">
+ <my-list1>
+ <name>Althea</name>
+ <my-leaf11>Jed</my-leaf11>
+ </my-list1>
+ </patch-cont>
+ </data>
+ <operation>created</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont</path>
+ <operation>deleted</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T13:50:24.917103+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf11</path>
+ <data>
+ <my-leaf11 xmlns="instance:identifier:patch:module">Jed</my-leaf11>
+ </data>
+ <operation>created</operation>
+ </data-changed-event>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name</path>
+ <data>
+ <name xmlns="instance:identifier:patch:module">Althea</name>
+ </data>
+ <operation>created</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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",
"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",
}
}
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T13:54:31.86969+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:name</path>
+ <data>
+ <name xmlns="instance:identifier:patch:module">Althea</name>
+ </data>
+ <operation>updated</operation>
+ </data-changed-event>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont/instance-identifier-patch-module:my-list1/instance-identifier-patch-module:my-list1[instance-identifier-patch-module:name='Althea']/instance-identifier-patch-module:my-leaf12</path>
+ <data>
+ <my-leaf12 xmlns="instance:identifier:patch:module">Bertha</my-leaf12>
+ </data>
+ <operation>created</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T13:41:50.132897+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont</path>
+ <data>
+ <patch-cont xmlns="instance:identifier:patch:module">
+ <my-list1>
+ <name>Althea</name>
+ <my-leaf11>Jed</my-leaf11>
+ <my-leaf12>Bertha</my-leaf12>
+ </my-list1>
+ </patch-cont>
+ </data>
+ <operation>updated</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont</path>
+ <operation>created</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont</path>
+ <operation>deleted</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file
{
- "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
--- /dev/null
+<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
+ <eventTime>2020-10-19T12:21:02.876756+02:00</eventTime>
+ <data-changed-notification>
+ <data-changed-event>
+ <path>/instance-identifier-patch-module:patch-cont</path>
+ <operation>updated</operation>
+ </data-changed-event>
+ </data-changed-notification>
+</notification>
\ No newline at end of file