From: Tom Pantelis Date: Sat, 3 Mar 2018 18:43:55 +0000 (-0500) Subject: Convert rfc8040 ListenerAdapter to DOMDataTreeChangeListener X-Git-Tag: release/fluorine~129 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;ds=sidebyside;h=4cbd10f1bd2b2bfb623e20e0828d603eb8879eec;p=netconf.git Convert rfc8040 ListenerAdapter to DOMDataTreeChangeListener DOMDataChangeListener is scheduled for removal - the bierman02 ListenerAdapter was already converted. Also modified the ListenerAdapter to use the org.json lib instead of the jackson lib as the UT revealed that the jackson lib doesn't convert the XML to JSON correctly and the bierman02 ListenerAdapter was recently converted to use org.json. Change-Id: I9e6778a206f412bb8498297cdcb94b77ebba25dd Signed-off-by: Tom Pantelis --- diff --git a/restconf/restconf-nb-rfc8040/pom.xml b/restconf/restconf-nb-rfc8040/pom.xml index e10030557f..1ee8149e24 100644 --- a/restconf/restconf-nb-rfc8040/pom.xml +++ b/restconf/restconf-nb-rfc8040/pom.xml @@ -26,18 +26,15 @@ org.opendaylight.netconf restconf-common-models - ${project.version} - + org.opendaylight.netconf restconf-common - ${project.version} - + org.opendaylight.netconf ietf-yang-library - ${project.version} - + org.opendaylight.yangtools @@ -113,7 +110,6 @@ org.apache.commons commons-lang3 - 3.0 @@ -126,6 +122,11 @@ aaa-shiro-api + + org.json + json + + org.glassfish.jersey.test-framework.providers @@ -140,7 +141,22 @@ org.glassfish.jersey.bundles.repackaged jersey-guava - 2.6 + test + + + org.opendaylight.controller + sal-binding-broker-impl + test-jar + test + + + org.opendaylight.controller + sal-binding-broker-impl + test + + + org.skyscreamer + jsonassert test diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/SubscribeToStreamUtil.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/SubscribeToStreamUtil.java index eb4b64f4aa..c02cb5dc3e 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/SubscribeToStreamUtil.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/SubscribeToStreamUtil.java @@ -28,8 +28,9 @@ import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; -import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener; import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction; +import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService; +import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeIdentifier; import org.opendaylight.controller.md.sal.dom.api.DOMNotificationListener; import org.opendaylight.restconf.common.context.InstanceIdentifierContext; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; @@ -324,16 +325,21 @@ public final class SubscribeToStreamUtil { * @param domDataBroker * data broker for register data change listener */ - @SuppressWarnings("deprecation") private static void registration(final LogicalDatastoreType ds, final DataChangeScope scope, final ListenerAdapter listener, final DOMDataBroker domDataBroker) { if (listener.isListening()) { return; } - final YangInstanceIdentifier path = listener.getPath(); - final ListenerRegistration registration = - domDataBroker.registerDataChangeListener(ds, path, listener, scope); + final DOMDataTreeChangeService changeService = (DOMDataTreeChangeService)domDataBroker.getSupportedExtensions() + .get(DOMDataTreeChangeService.class); + if (changeService == null) { + throw new UnsupportedOperationException("DOMDataBroker does not support the DOMDataTreeChangeService"); + } + + final DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(ds, listener.getPath()); + final ListenerRegistration registration = + changeService.registerDataTreeChangeListener(root, listener); listener.setRegistration(registration); } diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractCommonSubscriber.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractCommonSubscriber.java index ea3ad270ff..6e2004eea4 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractCommonSubscriber.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/AbstractCommonSubscriber.java @@ -29,8 +29,8 @@ abstract class AbstractCommonSubscriber extends AbstractQueryParams implements B @SuppressWarnings("rawtypes") private EventBusChangeRecorder eventBusChangeRecorder; - @SuppressWarnings("rawtypes") - private ListenerRegistration registration; + + private volatile ListenerRegistration registration; /** * Creating {@link EventBus}. @@ -51,8 +51,10 @@ abstract class AbstractCommonSubscriber extends AbstractQueryParams implements B @Override public final void close() throws Exception { - this.registration.close(); - this.registration = null; + if (this.registration != null) { + this.registration.close(); + this.registration = null; + } deleteDataInDS(); unregister(); @@ -93,8 +95,7 @@ abstract class AbstractCommonSubscriber extends AbstractQueryParams implements B * @param registration * DOMDataChangeListener registration */ - @SuppressWarnings("rawtypes") - public void setRegistration(final ListenerRegistration registration) { + public void setRegistration(final ListenerRegistration registration) { this.registration = registration; } diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java index 874ba1eaa8..0296e469d5 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapter.java @@ -7,19 +7,16 @@ */ package org.opendaylight.restconf.nb.rfc8040.streams.listeners; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.common.base.Preconditions; -import com.google.common.base.Throwables; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; +import java.util.Optional; import javax.xml.stream.XMLStreamException; import javax.xml.transform.dom.DOMResult; -import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent; -import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener; +import org.json.XML; +import org.opendaylight.controller.md.sal.dom.api.ClusteredDOMDataTreeChangeListener; 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; @@ -30,6 +27,8 @@ 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.DataSchemaContextTree; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; @@ -44,7 +43,7 @@ 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 DOMDataChangeListener { +public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener { private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class); @@ -52,8 +51,6 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData private final String streamName; private final NotificationOutputType outputType; - private AsyncDataChangeEvent> change; - /** * Creates new {@link ListenerAdapter} listener specified by path and stream * name and register for subscribing. @@ -77,10 +74,8 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData } @Override - @SuppressWarnings("checkstyle:hiddenField") - public void onDataChanged(final AsyncDataChangeEvent> change) { - this.change = change; - final String xml = prepareXml(); + public void onDataTreeChanged(final Collection dataTreeCandidates) { + final String xml = prepareXml(dataTreeCandidates); if (checkQueryParams(xml, this)) { prepareAndPostData(xml); } @@ -118,13 +113,7 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData private void prepareAndPostData(final String xml) { final Event event = new Event(EventType.NOTIFY); if (this.outputType.equals(NotificationOutputType.JSON)) { - try { - final JsonNode node = new XmlMapper().readTree(xml.getBytes(StandardCharsets.UTF_8)); - event.setData(node.toString()); - } catch (final IOException e) { - LOG.error("Error parsing XML {}", xml, e); - Throwables.propagate(e); - } + event.setData(XML.toJSONObject(xml).toString()); } else { event.setData(xml); } @@ -138,9 +127,11 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData /** * Prepare data in printable form and transform it to String. * + * @param dataTreeCandidates the DataTreeCandidates to transform + * * @return Data in printable form. */ - private String prepareXml() { + private String prepareXml(final Collection dataTreeCandidates) { final SchemaContext schemaContext = schemaHandler.get(); final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext); final Document doc = createDocument(); @@ -149,7 +140,7 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData final Element dataChangedNotificationEventElement = doc.createElementNS( "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification"); - addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, this.change, + addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, dataTreeCandidates, schemaContext, dataContextTree); notificationElement.appendChild(dataChangedNotificationEventElement); return transformDoc(doc); @@ -157,75 +148,83 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData /** * Adds values to data changed notification event element. - * - * @param doc - * {@link Document} - * @param dataChangedNotificationEventElement - * {@link Element} - * @param change - * {@link AsyncDataChangeEvent} */ @SuppressWarnings("checkstyle:hiddenField") private void addValuesToDataChangedNotificationEventElement(final Document doc, final Element dataChangedNotificationEventElement, - final AsyncDataChangeEvent> change, + final Collection dataTreeCandidates, final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) { - addCreatedChangedValuesFromDataToElement(doc, change.getCreatedData().entrySet(), - dataChangedNotificationEventElement, Operation.CREATED, schemaContext, dataSchemaContextTree); - - addCreatedChangedValuesFromDataToElement(doc, change.getUpdatedData().entrySet(), - dataChangedNotificationEventElement, Operation.UPDATED, schemaContext, dataSchemaContextTree); - - addValuesFromDataToElement(doc, change.getRemovedPaths(), dataChangedNotificationEventElement, - Operation.DELETED, schemaContext, dataSchemaContextTree); - } - - /** - * Adds values from data to element. - * - * @param doc - * {@link Document} - * @param data - * Set of {@link YangInstanceIdentifier}. - * @param element - * {@link Element} - * @param operation - * {@link Operation} - * @param schemaContext - * schema context - * @param dataSchemaContextTree - * data schema context tree - */ - private void addValuesFromDataToElement(final Document doc, final Set data, - final Element element, final Operation operation, final SchemaContext schemaContext, - final DataSchemaContextTree dataSchemaContextTree) { - if (data == null || data.isEmpty()) { - return; - } - for (final YangInstanceIdentifier yiid : data) { - if (!dataSchemaContextTree.getChild(yiid).isMixin()) { - final Node node = createDataChangeEventElement(doc, yiid, operation, schemaContext); - element.appendChild(node); + for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) { + DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode(); + if (candidateNode == null) { + continue; } + YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath(); + addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode, + yiid.getParent(), schemaContext, dataSchemaContextTree); } } - private void addCreatedChangedValuesFromDataToElement(final Document doc, - final Set>> data, final Element element, - final Operation operation, final SchemaContext schemaContext, + private void addNodeToDataChangeNotificationEventElement(final Document doc, + final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode, + final YangInstanceIdentifier parentYiid, final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) { - if (data == null || data.isEmpty()) { + + Optional> optionalNormalizedNode = Optional.empty(); + switch (candidateNode.getModificationType()) { + case APPEARED: + case SUBTREE_MODIFIED: + case WRITE: + optionalNormalizedNode = candidateNode.getDataAfter(); + break; + case DELETE: + case DISAPPEARED: + optionalNormalizedNode = candidateNode.getDataBefore(); + break; + case UNMODIFIED: + default: + break; + } + + if (!optionalNormalizedNode.isPresent()) { + LOG.error("No node present in notification for {}", candidateNode); return; } - for (final Entry> entry : data) { - if (!dataSchemaContextTree.getChild(entry.getKey()).isMixin() - && (!getLeafNodesOnly() || entry.getValue() instanceof LeafNode)) { - final Node node = createCreatedChangedDataChangeEventElement(doc, entry, operation, schemaContext, - dataSchemaContextTree); - element.appendChild(node); + + NormalizedNode normalizedNode = optionalNormalizedNode.get(); + YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid) + .append(normalizedNode.getIdentifier()).build(); + + boolean isNodeMixin = dataSchemaContextTree.getChild(yiid).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, Operation.DELETED, schemaContext); + break; + case UNMODIFIED: + default: + break; + } + if (node != null) { + dataChangedNotificationEventElement.appendChild(node); } } + + for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) { + addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, childNode, + yiid, schemaContext, dataSchemaContextTree); + } } /** @@ -256,11 +255,10 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData } private Node createCreatedChangedDataChangeEventElement(final Document doc, - final Entry> entry, final Operation operation, + final YangInstanceIdentifier eventPath, NormalizedNode normalized, final Operation operation, final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) { final Element dataChangeEventElement = doc.createElement("data-change-event"); final Element pathElement = doc.createElement("path"); - final YangInstanceIdentifier eventPath = entry.getKey(); addPathAsValueToElement(eventPath, pathElement, schemaContext); dataChangeEventElement.appendChild(pathElement); @@ -270,7 +268,6 @@ public class ListenerAdapter extends AbstractCommonSubscriber implements DOMData try { SchemaPath nodePath; - final NormalizedNode normalized = entry.getValue(); if (normalized instanceof MapEntryNode || normalized instanceof UnkeyedListEntryNode) { nodePath = dataSchemaContextTree.getChild(eventPath).getDataSchemaNode().getPath(); } else { diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImplTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImplTest.java index 24d9d8b9ee..bda60dd71d 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImplTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfStreamsSubscriptionServiceImplTest.java @@ -17,6 +17,7 @@ import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; import java.lang.reflect.Field; import java.net.URI; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -37,8 +38,8 @@ import org.mockito.MockitoAnnotations; import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; -import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener; import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction; +import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService; import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction; import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain; import org.opendaylight.restconf.common.context.NormalizedNodeContext; @@ -79,8 +80,8 @@ public class RestconfStreamsSubscriptionServiceImplTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - final TransactionChainHandler txHandler = Mockito.mock(TransactionChainHandler.class); - final DOMTransactionChain domTx = Mockito.mock(DOMTransactionChain.class); + final TransactionChainHandler txHandler = mock(TransactionChainHandler.class); + final DOMTransactionChain domTx = mock(DOMTransactionChain.class); Mockito.when(this.transactionHandler.get()).thenReturn(domTx); Mockito.when(txHandler.get()).thenReturn(domTx); final DOMDataWriteTransaction wTx = Mockito.mock(DOMDataWriteTransaction.class); @@ -92,16 +93,23 @@ public class RestconfStreamsSubscriptionServiceImplTest { Futures.immediateCheckedFuture(null); Mockito.when(rwTx.submit()).thenReturn(checkedFutureEmpty); Mockito.when(domTx.newReadWriteTransaction()).thenReturn(rwTx); - final CheckedFuture checked = Mockito.mock(CheckedFuture.class); + final CheckedFuture checked = mock(CheckedFuture.class); Mockito.when(wTx.submit()).thenReturn(checked); Mockito.when(checked.checkedGet()).thenReturn(null); this.schemaHandler = new SchemaContextHandler(txHandler); final DOMDataBroker dataBroker = mock(DOMDataBroker.class); - final ListenerRegistration listener = mock(ListenerRegistration.class); + + DOMDataTreeChangeService dataTreeChangeService = mock(DOMDataTreeChangeService.class); + doReturn(mock(ListenerRegistration.class)).when(dataTreeChangeService) + .registerDataTreeChangeListener(any(), any()); + + doReturn(Collections.singletonMap(DOMDataTreeChangeService.class, dataTreeChangeService)) + .when(dataBroker).getSupportedExtensions(); + doReturn(dataBroker).when(this.dataBrokerHandler).get(); - doReturn(listener).when(dataBroker).registerDataChangeListener(any(), any(), any(), any()); - final MultivaluedMap map = Mockito.mock(MultivaluedMap.class); + + final MultivaluedMap map = mock(MultivaluedMap.class); final Set>> set = new HashSet<>(); Mockito.when(map.entrySet()).thenReturn(set); Mockito.when(this.uriInfo.getQueryParameters()).thenReturn(map); diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java new file mode 100644 index 0000000000..1fb86d2732 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/streams/listeners/ListenerAdapterTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2017 Red Hat, Inc. 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 java.time.Instant.EPOCH; +import static org.junit.Assert.fail; + +import com.google.common.util.concurrent.Uninterruptibles; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.WriteTransaction; +import org.opendaylight.controller.md.sal.binding.test.AbstractConcurrentDataBrokerTest; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.TransactionChainListener; +import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; +import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService; +import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeIdentifier; +import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler; +import org.opendaylight.restconf.nb.rfc8040.handlers.TransactionChainHandler; +import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.PatchCont; +import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.patch.cont.MyList1; +import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.patch.cont.MyList1Builder; +import org.opendaylight.yang.gen.v1.instance.identifier.patch.module.rev151121.patch.cont.MyList1Key; +import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ListenerAdapterTest extends AbstractConcurrentDataBrokerTest { + private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapterTest.class); + + private static final String JSON_NOTIF_LEAVES_CREATE = "/listener-adapter-test/notif-leaves-create.json"; + private static final String JSON_NOTIF_LEAVES_UPDATE = "/listener-adapter-test/notif-leaves-update.json"; + private static final String JSON_NOTIF_LEAVES_DEL = "/listener-adapter-test/notif-leaves-del.json"; + private static final String JSON_NOTIF_CREATE = "/listener-adapter-test/notif-create.json"; + private static final String JSON_NOTIF_UPDATE = "/listener-adapter-test/notif-update.json"; + private static final String JSON_NOTIF_DEL = "/listener-adapter-test/notif-del.json"; + + private static YangInstanceIdentifier PATCH_CONT_YIID = + YangInstanceIdentifier.create(new YangInstanceIdentifier.NodeIdentifier(PatchCont.QNAME)); + + private DataBroker dataBroker; + private DOMDataBroker domDataBroker; + private TransactionChainHandler transactionChainHandler; + private SchemaContextHandler schemaContextHandler; + + @Before + public void setUp() throws Exception { + dataBroker = getDataBroker(); + domDataBroker = getDomBroker(); + SchemaContext sc = YangParserTestUtils.parseYangResource( + "/instanceidentifier/yang/instance-identifier-patch-module.yang"); + + transactionChainHandler = new TransactionChainHandler(domDataBroker.createTransactionChain( + Mockito.mock(TransactionChainListener.class))); + schemaContextHandler = new SchemaContextHandler(transactionChainHandler); + SchemaContextHandler.setSchemaContext(sc); + } + + class ListenerAdapterTester extends ListenerAdapter { + + private volatile String lastNotification; + private CountDownLatch notificationLatch = new CountDownLatch(1); + + ListenerAdapterTester(final YangInstanceIdentifier path, final String streamName, + final NotificationOutputTypeGrouping.NotificationOutputType outputType, + final boolean leafNodesOnly) { + super(path, streamName, outputType); + setQueryParams(EPOCH, Optional.empty(), Optional.empty(), leafNodesOnly); + } + + @Override + protected void post(final Event event) { + this.lastNotification = event.getData(); + notificationLatch.countDown(); + } + + public void assertGot(final String json) { + if (!Uninterruptibles.awaitUninterruptibly(notificationLatch, 5, TimeUnit.SECONDS)) { + fail("Timed out waiting for notification for: " + json); + } + + LOG.info("lastNotification: {}", lastNotification); + String withFakeDate = withFakeDate(lastNotification); + LOG.info("Comparing: \n{}\n{}", json, withFakeDate); + + JSONAssert.assertEquals(json, withFakeDate, false); + this.lastNotification = null; + notificationLatch = new CountDownLatch(1); + } + } + + static String withFakeDate(final String in) { + JSONObject doc = new JSONObject(in); + JSONObject notification = doc.getJSONObject("notification"); + if (notification == null) { + return in; + } + notification.put("eventTime", "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())); + return withFakeDate(new String(bytes, StandardCharsets.UTF_8)); + } + + @Test + public void testJsonNotifsLeaves() throws Exception { + ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey", + NotificationOutputTypeGrouping.NotificationOutputType.JSON, true); + adapter.setCloseVars(transactionChainHandler, schemaContextHandler); + + DOMDataTreeChangeService changeService = (DOMDataTreeChangeService) + domDataBroker.getSupportedExtensions().get(DOMDataTreeChangeService.class); + DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID); + changeService.registerDataTreeChangeListener(root, adapter); + + WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction(); + MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea"); + InstanceIdentifier iid = InstanceIdentifier.create(PatchCont.class) + .child(MyList1.class, new MyList1Key("Althea")); + writeTransaction.put(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true); + writeTransaction.submit(); + adapter.assertGot(getNotifJson(JSON_NOTIF_LEAVES_CREATE)); + + writeTransaction = dataBroker.newWriteOnlyTransaction(); + builder = new MyList1Builder().setKey(new MyList1Key("Althea")).setMyLeaf12("Bertha"); + writeTransaction.merge(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true); + writeTransaction.submit(); + adapter.assertGot(getNotifJson(JSON_NOTIF_LEAVES_UPDATE)); + + writeTransaction = dataBroker.newWriteOnlyTransaction(); + writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid); + writeTransaction.submit(); + adapter.assertGot(getNotifJson(JSON_NOTIF_LEAVES_DEL)); + } + + @Test + public void testJsonNotifs() throws Exception { + ListenerAdapterTester adapter = new ListenerAdapterTester(PATCH_CONT_YIID, "Casey", + NotificationOutputTypeGrouping.NotificationOutputType.JSON, false); + adapter.setCloseVars(transactionChainHandler, schemaContextHandler); + + DOMDataTreeChangeService changeService = (DOMDataTreeChangeService) + domDataBroker.getSupportedExtensions().get(DOMDataTreeChangeService.class); + DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, PATCH_CONT_YIID); + changeService.registerDataTreeChangeListener(root, adapter); + + WriteTransaction writeTransaction = dataBroker.newWriteOnlyTransaction(); + MyList1Builder builder = new MyList1Builder().setMyLeaf11("Jed").setName("Althea"); + InstanceIdentifier iid = InstanceIdentifier.create(PatchCont.class) + .child(MyList1.class, new MyList1Key("Althea")); + writeTransaction.put(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true); + writeTransaction.submit(); + adapter.assertGot(getNotifJson(JSON_NOTIF_CREATE)); + + writeTransaction = dataBroker.newWriteOnlyTransaction(); + builder = new MyList1Builder().setKey(new MyList1Key("Althea")).setMyLeaf12("Bertha"); + writeTransaction.merge(LogicalDatastoreType.CONFIGURATION, iid, builder.build(), true); + writeTransaction.submit(); + adapter.assertGot(getNotifJson(JSON_NOTIF_UPDATE)); + + writeTransaction = dataBroker.newWriteOnlyTransaction(); + writeTransaction.delete(LogicalDatastoreType.CONFIGURATION, iid); + writeTransaction.submit(); + adapter.assertGot(getNotifJson(JSON_NOTIF_DEL)); + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json new file mode 100644 index 0000000000..31a7f1a412 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-create.json @@ -0,0 +1,55 @@ +{ + "notification": { + "data-changed-notification": { + "data-change-event": [ + { + "data": { + "my-leaf11": { + "content": "Jed", + "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-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']" + } + ], + "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" + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json new file mode 100644 index 0000000000..8c2af38b35 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-del.json @@ -0,0 +1,36 @@ +{ + "notification": { + "data-changed-notification": { + "data-change-event": [ + { + "data": { + "patch-cont": { + "xmlns": "instance:identifier:patch:module" + } + }, + "operation": "updated", + "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" + } + ], + "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" + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json new file mode 100644 index 0000000000..f685b89605 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-create.json @@ -0,0 +1,31 @@ +{ + "notification": { + "data-changed-notification": { + "data-change-event": [ + { + "data": { + "my-leaf11": { + "content": "Jed", + "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-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" + } + ], + "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" + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json new file mode 100644 index 0000000000..fd1f1d8e0d --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-del.json @@ -0,0 +1,23 @@ +{ + "notification": { + "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-leaf11" + }, + { + "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" + } + ], + "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" + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json new file mode 100644 index 0000000000..890d945f7e --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-leaves-update.json @@ -0,0 +1,31 @@ +{ + "notification": { + "data-changed-notification": { + "data-change-event": [ + { + "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" + } + ], + "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" + } +} diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json new file mode 100644 index 0000000000..0ee0547f1b --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/listener-adapter-test/notif-update.json @@ -0,0 +1,57 @@ +{ + "notification": { + "data-changed-notification": { + "data-change-event": [ + { + "data": { + "patch-cont": { + "my-list1": { + "my-leaf11": "Jed", + "my-leaf12": "Bertha", + "name": "Althea" + }, + "xmlns": "instance:identifier:patch:module" + } + }, + "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" + } + ], + "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" + } +}