From 655930e448934c1187a669f0186c16a64ba6806b Mon Sep 17 00:00:00 2001 From: Jakub Toth Date: Thu, 20 Oct 2016 10:54:06 +0200 Subject: [PATCH] Bug 6324 - Notifications stream output is not same as restconf data * serialization NormalizedNode to JSON with gson codec from yangtools * added test Change-Id: I15245c48188151a1e7aaaf482997ae0d214491c0 Signed-off-by: Jakub Toth --- .../NotificationListenerAdapter.java | 56 +++- .../sal/streams/listeners/Notificator.java | 1 - .../listeners/NotificationListenerTest.java | 261 ++++++++++++++++++ .../notifications/notifi-module.yang | 49 ++++ 4 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/streams/listeners/NotificationListenerTest.java create mode 100644 restconf/sal-rest-connector/src/test/resources/notifications/notifi-module.yang diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/NotificationListenerAdapter.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/NotificationListenerAdapter.java index 3e0bdd9847..edafaaaa99 100644 --- a/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/NotificationListenerAdapter.java +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/NotificationListenerAdapter.java @@ -18,9 +18,13 @@ import io.netty.util.internal.ConcurrentSet; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.io.StringWriter; import java.io.UnsupportedEncodingException; +import java.io.Writer; import java.util.Collection; import java.util.Date; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import javax.xml.stream.XMLOutputFactory; @@ -34,10 +38,10 @@ import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.json.JSONObject; -import org.json.XML; import org.opendaylight.controller.md.sal.dom.api.DOMNotification; import org.opendaylight.controller.md.sal.dom.api.DOMNotificationListener; import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.yangtools.concepts.ListenerRegistration; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; @@ -46,6 +50,9 @@ import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory; import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils; import org.opendaylight.yangtools.yang.model.api.SchemaContext; @@ -67,14 +74,17 @@ public class NotificationListenerAdapter implements DOMNotificationListener { private static final TransformerFactory FACTORY = TransformerFactory.newInstance(); private final String streamName; - private ListenerRegistration registration; - private Set subscribers = new ConcurrentSet<>(); private final EventBus eventBus; private final EventBusChangeRecorder eventBusChangeRecorder; private final SchemaPath path; private final String outputType; + private SchemaContext schemaContext; + private DOMNotification notification; + private ListenerRegistration registration; + private Set subscribers = new ConcurrentSet<>(); + /** * Set path of listener and stream name, register event bus. * @@ -98,17 +108,44 @@ public class NotificationListenerAdapter implements DOMNotificationListener { @Override public void onNotification(final DOMNotification notification) { - final String xml = prepareXmlFrom(notification); + this.schemaContext = ControllerContext.getInstance().getGlobalSchema(); + this.notification = notification; final Event event = new Event(EventType.NOTIFY); if (this.outputType.equals("JSON")) { - final JSONObject jsonObject = XML.toJSONObject(xml); - event.setData(jsonObject.toString()); + event.setData(prepareJson()); } else { - event.setData(xml); + event.setData(prepareXml()); } this.eventBus.post(event); } + /** + * Prepare json from notification data + * + * @return json as {@link String} + */ + private String prepareJson() { + final JSONObject json = new JSONObject(); + json.put("ietf-restconf:notification", + new JSONObject(writeBodyToString()).put("event-time", ListenerAdapter.toRFC3339(new Date()))); + return json.toString(); + } + + private String writeBodyToString() { + final Writer writer = new StringWriter(); + final NormalizedNodeStreamWriter jsonStream = + JSONNormalizedNodeStreamWriter.createExclusiveWriter(JSONCodecFactory.create(this.schemaContext), + this.notification.getType(), null, JsonWriterFactory.createJsonWriter(writer)); + final NormalizedNodeWriter nodeWriter = NormalizedNodeWriter.forStreamWriter(jsonStream); + try { + nodeWriter.write(this.notification.getBody()); + nodeWriter.close(); + } catch (final IOException e) { + throw new RestconfDocumentedException("Problem while writing body of notification to JSON. ", e); + } + return writer.toString(); + } + /** * Checks if exists at least one {@link Channel} subscriber. * @@ -197,8 +234,7 @@ public class NotificationListenerAdapter implements DOMNotificationListener { this.eventBus.post(event); } - private String prepareXmlFrom(final DOMNotification notification) { - final SchemaContext schemaContext = ControllerContext.getInstance().getGlobalSchema(); + private String prepareXml() { final Document doc = ListenerAdapter.createDocument(); final Element notificationElement = doc.createElementNS("urn:ietf:params:xml:ns:netconf:notification:1.0", @@ -211,7 +247,7 @@ public class NotificationListenerAdapter implements DOMNotificationListener { final Element notificationEventElement = doc.createElementNS( "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "create-notification-stream"); - addValuesToNotificationEventElement(doc, notificationEventElement, notification, schemaContext); + addValuesToNotificationEventElement(doc, notificationEventElement, this.notification, this.schemaContext); notificationElement.appendChild(notificationEventElement); try { diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/Notificator.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/Notificator.java index 0bd38652b7..22a27c966e 100644 --- a/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/Notificator.java +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/streams/listeners/Notificator.java @@ -166,7 +166,6 @@ public class Notificator { */ public static boolean existNotificationListenerFor(final String streamName) { return notificationListenersByStreamName.containsKey(streamName); - } public static List createNotificationListener(final List paths, diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/streams/listeners/NotificationListenerTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/streams/listeners/NotificationListenerTest.java new file mode 100644 index 0000000000..2cad9d7b62 --- /dev/null +++ b/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/streams/listeners/NotificationListenerTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2016 Cisco Systems, 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.controller.sal.streams.listeners; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URI; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.opendaylight.controller.md.sal.dom.api.DOMNotification; +import org.opendaylight.controller.sal.restconf.impl.test.TestUtils; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.netconf.sal.streams.listeners.NotificationListenerAdapter; +import org.opendaylight.netconf.sal.streams.listeners.Notificator; +import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.CreateNotificationStreamInput1.NotificationOutputType; +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.data.api.YangInstanceIdentifier.AugmentationIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +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.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaPath; + +public class NotificationListenerTest { + + private SchemaContext schmeaCtx; + + @Before + public void init() throws Exception { + MockitoAnnotations.initMocks(this); + ControllerContext.getInstance().setGlobalSchema(TestUtils.loadSchemaContext("/notifications")); + this.schmeaCtx = ControllerContext.getInstance().getGlobalSchema(); + } + + @Test + public void notifi_leafTest() throws Exception { + final QNameModule moduleQName = + QNameModule.create(new URI("notifi:mod"), new SimpleDateFormat("yyyy-MM-dd").parse("2016-11-23")); + final SchemaPath schemaPathNotifi = SchemaPath.create(false, QName.create(moduleQName, "notifi-leaf")); + + final DOMNotification notificationData = mock(DOMNotification.class); + + final LeafNode leaf = mockLeaf(QName.create(moduleQName, "lf")); + final ContainerNode notifiBody = mockCont(schemaPathNotifi.getLastComponent(), leaf); + + when(notificationData.getType()).thenReturn(schemaPathNotifi); + when(notificationData.getBody()).thenReturn(notifiBody); + + final String result = prepareJson(notificationData, schemaPathNotifi); + + assertTrue(result.contains("ietf-restconf:notification")); + assertTrue(result.contains("event-time")); + assertTrue(result.contains("notifi-module:notifi-leaf")); + assertTrue(result.contains("lf" + '"' + ":" + '"' + "value")); + } + + @Test + public void notifi_cont_leafTest() throws Exception { + final QNameModule moduleQName = + QNameModule.create(new URI("notifi:mod"), new SimpleDateFormat("yyyy-MM-dd").parse("2016-11-23")); + + final SchemaPath schemaPathNotifi = + SchemaPath.create(false, QName.create(moduleQName, "notifi-cont")); + + final DOMNotification notificationData = mock(DOMNotification.class); + + final LeafNode leaf = mockLeaf(QName.create(moduleQName, "lf")); + final ContainerNode cont = mockCont(QName.create(moduleQName, "cont"), leaf); + final ContainerNode notifiBody = mockCont(schemaPathNotifi.getLastComponent(), cont); + + when(notificationData.getType()).thenReturn(schemaPathNotifi); + when(notificationData.getBody()).thenReturn(notifiBody); + + final String result = prepareJson(notificationData, schemaPathNotifi); + + assertTrue(result.contains("ietf-restconf:notification")); + assertTrue(result.contains("event-time")); + assertTrue(result.contains("notifi-module:notifi-cont")); + assertTrue(result.contains("cont")); + assertTrue(result.contains("lf" + '"' + ":" + '"' + "value")); + } + + @Test + public void notifi_list_Test() throws Exception { + final QNameModule moduleQName = + QNameModule.create(new URI("notifi:mod"), new SimpleDateFormat("yyyy-MM-dd").parse("2016-11-23")); + + final SchemaPath schemaPathNotifi = SchemaPath.create(false, QName.create(moduleQName, "notifi-list")); + + final DOMNotification notificationData = mock(DOMNotification.class); + + final LeafNode leaf = mockLeaf(QName.create(moduleQName, "lf")); + final MapEntryNode entry = mockMapEntry(QName.create(moduleQName, "lst"), leaf); + final MapNode list = mockList(QName.create(moduleQName, "lst"), entry); + final ContainerNode cont = mockCont(QName.create(moduleQName, "cont"), list); + final ContainerNode notifiBody = mockCont(schemaPathNotifi.getLastComponent(), cont); + + when(notificationData.getType()).thenReturn(schemaPathNotifi); + when(notificationData.getBody()).thenReturn(notifiBody); + + final String result = prepareJson(notificationData, schemaPathNotifi); + + assertTrue(result.contains("ietf-restconf:notification")); + assertTrue(result.contains("event-time")); + assertTrue(result.contains("notifi-module:notifi-list")); + assertTrue(result.contains("lst")); + assertTrue(result.contains("lf" + '"' + ":" + '"' + "value")); + } + + @Test + public void notifi_grpTest() throws Exception { + final QNameModule moduleQName = + QNameModule.create(new URI("notifi:mod"), new SimpleDateFormat("yyyy-MM-dd").parse("2016-11-23")); + + final SchemaPath schemaPathNotifi = SchemaPath.create(false, QName.create(moduleQName, "notifi-grp")); + + final DOMNotification notificationData = mock(DOMNotification.class); + + final LeafNode leaf = mockLeaf(QName.create(moduleQName, "lf")); + final ContainerNode notifiBody = mockCont(schemaPathNotifi.getLastComponent(), leaf); + + when(notificationData.getType()).thenReturn(schemaPathNotifi); + when(notificationData.getBody()).thenReturn(notifiBody); + + final String result = prepareJson(notificationData, schemaPathNotifi); + + assertTrue(result.contains("ietf-restconf:notification")); + assertTrue(result.contains("event-time")); + assertTrue(result.contains("lf" + '"' + ":" + '"' + "value")); + } + + @Test + public void notifi_augmTest() throws Exception { + final QNameModule moduleQName = + QNameModule.create(new URI("notifi:mod"), new SimpleDateFormat("yyyy-MM-dd").parse("2016-11-23")); + + final SchemaPath schemaPathNotifi = SchemaPath.create(false, QName.create(moduleQName, "notifi-augm")); + + final DOMNotification notificationData = mock(DOMNotification.class); + + final LeafNode leaf = mockLeaf(QName.create(moduleQName, "lf-augm")); + final AugmentationNode augm = mockAugm(leaf); + final ContainerNode notifiBody = mockCont(schemaPathNotifi.getLastComponent(), augm); + + when(notificationData.getType()).thenReturn(schemaPathNotifi); + when(notificationData.getBody()).thenReturn(notifiBody); + + final String result = prepareJson(notificationData, schemaPathNotifi); + + assertTrue(result.contains("ietf-restconf:notification")); + assertTrue(result.contains("event-time")); + assertTrue(result.contains("lf-augm" + '"' + ":" + '"' + "value")); + } + + private AugmentationNode mockAugm(final LeafNode leaf) { + final AugmentationNode augm = mock(AugmentationNode.class); + final AugmentationIdentifier augmId = new AugmentationIdentifier(SingletonSet.of(leaf.getNodeType())); + when(augm.getIdentifier()).thenReturn(augmId); + + final Collection> childs = new ArrayList<>(); + childs.add(leaf); + + when(augm.getValue()).thenReturn(childs); + return augm; + } + + private MapEntryNode mockMapEntry(final QName entryQName, final LeafNode leaf) { + final MapEntryNode entry = mock(MapEntryNode.class); + final Map keyValues = new HashMap<>(); + keyValues.put(leaf.getNodeType(), "value"); + final NodeIdentifierWithPredicates nodeId = new NodeIdentifierWithPredicates(leaf.getNodeType(), keyValues); + when(entry.getIdentifier()).thenReturn(nodeId); + when(entry.getChild(any())).thenReturn(Optional.of(leaf)); + + final Collection> childs = new ArrayList<>(); + childs.add(leaf); + + when(entry.getValue()).thenReturn(childs); + return entry; + } + + private MapNode mockList(final QName listQName, final MapEntryNode... entries) { + final MapNode list = mock(MapNode.class); + when(list.getIdentifier()).thenReturn(NodeIdentifier.create(listQName)); + when(list.getNodeType()).thenReturn(listQName); + when(list.getValue()).thenReturn(Lists.newArrayList(entries)); + return list; + } + + private ContainerNode mockCont(final QName contQName, final DataContainerChild child) { + final ContainerNode cont = mock(ContainerNode.class); + when(cont.getIdentifier()).thenReturn(NodeIdentifier.create(contQName)); + when(cont.getNodeType()).thenReturn(contQName); + + final Collection> childs = new ArrayList<>(); + childs.add(child); + when(cont.getValue()).thenReturn(childs); + return cont; + } + + private LeafNode mockLeaf(final QName leafQName) { + final LeafNode child = mock(LeafNode.class); + when(child.getNodeType()).thenReturn(leafQName); + when(child.getIdentifier()).thenReturn(NodeIdentifier.create(leafQName)); + when(child.getValue()).thenReturn("value"); + return child; + } + + private String prepareJson(final DOMNotification notificationData, final SchemaPath schemaPathNotifi) + throws Exception { + final List paths = new ArrayList<>(); + paths.add(schemaPathNotifi); + final List listNotifi = + Notificator.createNotificationListener(paths, "stream-name", NotificationOutputType.JSON.toString()); + final NotificationListenerAdapter notifi = listNotifi.get(0); + + final Class vars[] = {}; + final Method prepareJsonM = notifi.getClass().getDeclaredMethod("prepareJson", vars); + prepareJsonM.setAccessible(true); + + final Field notification = notifi.getClass().getDeclaredField("notification"); + notification.setAccessible(true); + notification.set(notifi, notificationData); + + final Field schema = notifi.getClass().getDeclaredField("schemaContext"); + schema.setAccessible(true); + schema.set(notifi, this.schmeaCtx); + + final String result = (String) prepareJsonM.invoke(notifi, null); + Preconditions.checkNotNull(result); + return result; + } +} diff --git a/restconf/sal-rest-connector/src/test/resources/notifications/notifi-module.yang b/restconf/sal-rest-connector/src/test/resources/notifications/notifi-module.yang new file mode 100644 index 0000000000..70d45f1631 --- /dev/null +++ b/restconf/sal-rest-connector/src/test/resources/notifications/notifi-module.yang @@ -0,0 +1,49 @@ +module notifi-module { + namespace "notifi:mod"; + prefix notm; + + revision "2016-11-23" { + } + + notification notifi-leaf { + leaf lf { + type string; + } + } + + notification notifi-cont { + container cont { + leaf lf { + type string; + } + } + } + + notification notifi-list { + list lst { + key lf; + leaf lf { + type string; + } + } + } + + notification notifi-grp { + uses grp; + } + + grouping grp { + leaf lf { + type string; + } + } + + notification notifi-augm { + } + + augment notifi-augm { + leaf lf-augm { + type string; + } + } +} \ No newline at end of file -- 2.36.6