2 * Copyright (c) 2014, 2016 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.streams.listeners;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static java.util.Objects.requireNonNull;
14 import com.google.common.base.MoreObjects;
15 import java.io.IOException;
16 import java.time.Instant;
17 import java.util.Collection;
18 import java.util.Map.Entry;
19 import java.util.Optional;
20 import javax.xml.stream.XMLStreamException;
21 import javax.xml.transform.dom.DOMResult;
23 import org.opendaylight.mdsal.dom.api.ClusteredDOMDataTreeChangeListener;
24 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
30 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
31 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
33 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
35 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
36 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
37 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
38 import org.opendaylight.yangtools.yang.model.api.Module;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.w3c.dom.Document;
44 import org.w3c.dom.Element;
45 import org.w3c.dom.Node;
48 * {@link ListenerAdapter} is responsible to track events, which occurred by changing data in data source.
50 public class ListenerAdapter extends AbstractCommonSubscriber implements ClusteredDOMDataTreeChangeListener {
52 private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
54 private final YangInstanceIdentifier path;
55 private final String streamName;
56 private final NotificationOutputType outputType;
59 * Creates new {@link ListenerAdapter} listener specified by path and stream name and register for subscribing.
61 * @param path Path to data in data store.
62 * @param streamName The name of the stream.
63 * @param outputType Type of output on notification (JSON, XML).
65 ListenerAdapter(final YangInstanceIdentifier path, final String streamName,
66 final NotificationOutputType outputType) {
67 setLocalNameOfPath(path.getLastPathArgument().getNodeType().getLocalName());
69 this.outputType = requireNonNull(outputType);
70 this.path = requireNonNull(path);
71 this.streamName = requireNonNull(streamName);
72 checkArgument(!streamName.isEmpty());
76 public void onDataTreeChanged(final Collection<DataTreeCandidate> dataTreeCandidates) {
77 final Instant now = Instant.now();
78 if (!checkStartStop(now, this)) {
82 final String xml = prepareXml(dataTreeCandidates);
83 if (checkFilter(xml)) {
84 prepareAndPostData(xml);
89 * Gets the name of the stream.
91 * @return The name of the stream.
94 public String getStreamName() {
95 return this.streamName;
99 public String getOutputType() {
100 return this.outputType.getName();
104 * Get path pointed to data in data store.
106 * @return Path pointed to data in data store.
108 public YangInstanceIdentifier getPath() {
113 * Prepare data of notification and data to client.
115 * @param xml XML-formatted data.
117 private void prepareAndPostData(final String xml) {
118 if (this.outputType.equals(NotificationOutputType.JSON)) {
119 post(XML.toJSONObject(xml).toString());
126 * Prepare data in printable form and transform it to String.
128 * @param dataTreeCandidates Data-tree candidates to be transformed.
129 * @return Data in printable form.
131 private String prepareXml(final Collection<DataTreeCandidate> dataTreeCandidates) {
132 final SchemaContext schemaContext = schemaHandler.get();
133 final DataSchemaContextTree dataContextTree = DataSchemaContextTree.from(schemaContext);
134 final Document doc = createDocument();
135 final Element notificationElement = basePartDoc(doc);
137 final Element dataChangedNotificationEventElement = doc.createElementNS(
138 "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
140 addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, dataTreeCandidates,
141 schemaContext, dataContextTree);
142 notificationElement.appendChild(dataChangedNotificationEventElement);
143 return transformDoc(doc);
147 * Adds values to data changed notification event element.
149 private void addValuesToDataChangedNotificationEventElement(final Document doc,
150 final Element dataChangedNotificationEventElement, final Collection<DataTreeCandidate> dataTreeCandidates,
151 final SchemaContext schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
153 for (DataTreeCandidate dataTreeCandidate : dataTreeCandidates) {
154 DataTreeCandidateNode candidateNode = dataTreeCandidate.getRootNode();
155 if (candidateNode == null) {
158 YangInstanceIdentifier yiid = dataTreeCandidate.getRootPath();
159 addNodeToDataChangeNotificationEventElement(doc, dataChangedNotificationEventElement, candidateNode,
160 yiid.getParent(), schemaContext, dataSchemaContextTree);
164 private void addNodeToDataChangeNotificationEventElement(final Document doc,
165 final Element dataChangedNotificationEventElement, final DataTreeCandidateNode candidateNode,
166 final YangInstanceIdentifier parentYiid, final SchemaContext schemaContext,
167 final DataSchemaContextTree dataSchemaContextTree) {
169 Optional<NormalizedNode<?, ?>> optionalNormalizedNode = Optional.empty();
170 switch (candidateNode.getModificationType()) {
172 case SUBTREE_MODIFIED:
174 optionalNormalizedNode = candidateNode.getDataAfter();
178 optionalNormalizedNode = candidateNode.getDataBefore();
185 if (!optionalNormalizedNode.isPresent()) {
186 LOG.error("No node present in notification for {}", candidateNode);
190 NormalizedNode<?, ?> normalizedNode = optionalNormalizedNode.get();
191 YangInstanceIdentifier yiid = YangInstanceIdentifier.builder(parentYiid)
192 .append(normalizedNode.getIdentifier()).build();
194 final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(yiid);
195 checkState(childrenSchemaNode.isPresent());
196 boolean isNodeMixin = childrenSchemaNode.get().isMixin();
197 boolean isSkippedNonLeaf = getLeafNodesOnly() && !(normalizedNode instanceof LeafNode);
198 if (!isNodeMixin && !isSkippedNonLeaf) {
200 switch (candidateNode.getModificationType()) {
202 case SUBTREE_MODIFIED:
204 Operation op = candidateNode.getDataBefore().isPresent() ? Operation.UPDATED : Operation.CREATED;
205 node = createCreatedChangedDataChangeEventElement(doc, yiid, normalizedNode, op,
206 schemaContext, dataSchemaContextTree);
210 node = createDataChangeEventElement(doc, yiid, schemaContext);
217 dataChangedNotificationEventElement.appendChild(node);
221 for (DataTreeCandidateNode childNode : candidateNode.getChildNodes()) {
222 addNodeToDataChangeNotificationEventElement(
223 doc, dataChangedNotificationEventElement, childNode, yiid, schemaContext, dataSchemaContextTree);
228 * Creates data-changed event element from data.
230 * @param doc {@link Document}
231 * @param schemaContext Schema context.
232 * @return {@link Node} represented by changed event element.
234 private static Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
235 final SchemaContext schemaContext) {
236 final Element dataChangeEventElement = doc.createElement("data-change-event");
237 final Element pathElement = doc.createElement("path");
238 addPathAsValueToElement(eventPath, pathElement, schemaContext);
239 dataChangeEventElement.appendChild(pathElement);
241 final Element operationElement = doc.createElement("operation");
242 operationElement.setTextContent(Operation.DELETED.value);
243 dataChangeEventElement.appendChild(operationElement);
245 return dataChangeEventElement;
248 private Node createCreatedChangedDataChangeEventElement(final Document doc, final YangInstanceIdentifier eventPath,
249 final NormalizedNode<?, ?> normalized, final Operation operation, final SchemaContext schemaContext,
250 final DataSchemaContextTree dataSchemaContextTree) {
251 final Element dataChangeEventElement = doc.createElement("data-change-event");
252 final Element pathElement = doc.createElement("path");
253 addPathAsValueToElement(eventPath, pathElement, schemaContext);
254 dataChangeEventElement.appendChild(pathElement);
256 final Element operationElement = doc.createElement("operation");
257 operationElement.setTextContent(operation.value);
258 dataChangeEventElement.appendChild(operationElement);
262 final Optional<DataSchemaContextNode<?>> childrenSchemaNode = dataSchemaContextTree.findChild(eventPath);
263 checkState(childrenSchemaNode.isPresent());
264 if (normalized instanceof MapEntryNode || normalized instanceof UnkeyedListEntryNode) {
265 nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath();
267 nodePath = childrenSchemaNode.get().getDataSchemaNode().getPath().getParent();
269 final DOMResult domResult = writeNormalizedNode(normalized, schemaContext, nodePath);
270 final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
271 final Element dataElement = doc.createElement("data");
272 dataElement.appendChild(result);
273 dataChangeEventElement.appendChild(dataElement);
274 } catch (final IOException e) {
275 LOG.error("Error in writer ", e);
276 } catch (final XMLStreamException e) {
277 LOG.error("Error processing stream", e);
280 return dataChangeEventElement;
284 * Adds path as value to element.
286 * @param eventPath Path to data in data store.
287 * @param element {@link Element}
288 * @param schemaContext Schema context.
290 private static void addPathAsValueToElement(final YangInstanceIdentifier eventPath, final Element element,
291 final SchemaContext schemaContext) {
292 final StringBuilder textContent = new StringBuilder();
294 for (final PathArgument pathArgument : eventPath.getPathArguments()) {
295 if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
298 textContent.append("/");
299 writeIdentifierWithNamespacePrefix(textContent, pathArgument.getNodeType(), schemaContext);
300 if (pathArgument instanceof NodeIdentifierWithPredicates) {
301 for (final Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) pathArgument).entrySet()) {
302 final QName keyValue = entry.getKey();
303 final String predicateValue = String.valueOf(entry.getValue());
304 textContent.append("[");
305 writeIdentifierWithNamespacePrefix(textContent, keyValue, schemaContext);
306 textContent.append("='").append(predicateValue).append("']");
308 } else if (pathArgument instanceof NodeWithValue) {
309 textContent.append("[.='").append(((NodeWithValue<?>) pathArgument).getValue()).append("']");
312 element.setTextContent(textContent.toString());
316 * Writes identifier that consists of prefix and QName.
318 * @param textContent Text builder that should be supplemented by QName and its modules name.
319 * @param qualifiedName QName of the element.
320 * @param schemaContext Schema context that holds modules which should contain module specified in QName.
322 private static void writeIdentifierWithNamespacePrefix(final StringBuilder textContent, final QName qualifiedName,
323 final SchemaContext schemaContext) {
324 final Optional<Module> module = schemaContext.findModule(qualifiedName.getModule());
325 if (module.isPresent()) {
326 textContent.append(module.get().getName());
327 textContent.append(":");
328 textContent.append(qualifiedName.getLocalName());
330 LOG.error("Cannot write identifier with namespace prefix in data-change listener adapter: "
331 + "Cannot find module in schema context for input QName {}.", qualifiedName);
332 throw new IllegalStateException(String.format("Cannot find module in schema context for input QName %s.",
338 * Consists of three types {@link Operation#CREATED}, {@link Operation#UPDATED} and {@link Operation#DELETED}.
340 private enum Operation {
345 private final String value;
347 Operation(final String value) {
353 public String toString() {
354 return MoreObjects.toStringHelper(this)
356 .add("stream-name", streamName)
357 .add("output-type", outputType)