Merge "Optimize NetconfMonitoringServiceImpl getSchemaForCapability"
[netconf.git] / netconf / netconf-util / src / main / java / org / opendaylight / netconf / util / messages / SubtreeFilter.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.netconf.util.messages;
10
11 import com.google.common.base.Optional;
12 import java.io.IOException;
13 import java.util.Map;
14 import org.opendaylight.controller.config.util.xml.DocumentedException;
15 import org.opendaylight.controller.config.util.xml.XmlElement;
16 import org.opendaylight.controller.config.util.xml.XmlUtil;
17 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
18 import org.opendaylight.netconf.util.mapping.AbstractNetconfOperation.OperationNameAndNamespace;
19 import org.slf4j.Logger;
20 import org.slf4j.LoggerFactory;
21 import org.w3c.dom.Attr;
22 import org.w3c.dom.Document;
23 import org.w3c.dom.Element;
24 import org.w3c.dom.Node;
25 import org.xml.sax.SAXException;
26
27 /**
28  * See <a href="http://tools.ietf.org/html/rfc6241#section-6">rfc6241</a> for details.
29  */
30 public class SubtreeFilter {
31     private static final Logger LOG = LoggerFactory.getLogger(SubtreeFilter.class);
32
33     public static Document applyRpcSubtreeFilter(Document requestDocument, Document rpcReply) throws DocumentedException {
34         OperationNameAndNamespace operationNameAndNamespace = new OperationNameAndNamespace(requestDocument);
35         if (XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0.equals(operationNameAndNamespace.getNamespace()) &&
36                 XmlNetconfConstants.GET.equals(operationNameAndNamespace.getOperationName()) ||
37                 XmlNetconfConstants.GET_CONFIG.equals(operationNameAndNamespace.getOperationName())) {
38             // process subtree filtering here, in case registered netconf operations do
39             // not implement filtering.
40             Optional<XmlElement> maybeFilter = operationNameAndNamespace.getOperationElement().getOnlyChildElementOptionally(
41                     XmlNetconfConstants.FILTER, XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0);
42             if (!maybeFilter.isPresent()) {
43                 return rpcReply;
44             }
45
46             rpcReply = reReadDocument(rpcReply);
47             XmlElement filter = maybeFilter.get();
48             if (isSupported(filter)) {
49
50                 // do
51                 return filtered(maybeFilter.get(), rpcReply);
52             }
53         }
54
55         return rpcReply; // return identical document
56     }
57
58     /**
59      * Filters notification content. If filter type isn't of type "subtree", returns unchanged notification content.
60      * If no match is found, absent is returned.
61      * @param filter filter
62      * @param notification notification
63      * @return document containing filtered notification content
64      * @throws DocumentedException
65      */
66     public static Optional<Document> applySubtreeNotificationFilter(XmlElement filter, Document notification) throws DocumentedException {
67         notification = reReadDocument(notification);
68         removeEventTimeNode(notification);
69         if (isSupported(filter)) {
70             return Optional.fromNullable(filteredNotification(filter, notification));
71         }
72         return Optional.of(extractNotificationContent(notification));
73     }
74
75     private static Document reReadDocument(Document notification) throws DocumentedException {
76         // FIXME: rpcReply document must be reread otherwise some nodes do not inherit namespaces. (services/service)
77         try {
78             notification = XmlUtil.readXmlToDocument(XmlUtil.toString(notification, true));
79         } catch (SAXException | IOException e) {
80             LOG.error("Cannot transform document", e);
81             throw new DocumentedException("Cannot transform document" + e);
82         }
83         return notification;
84     }
85
86     private static void removeEventTimeNode(Document document) {
87         final Node eventTimeNode = document.getDocumentElement().getElementsByTagNameNS(
88                 XmlNetconfConstants.URN_IETF_PARAMS_NETCONF_CAPABILITY_NOTIFICATION_1_0, XmlNetconfConstants.EVENT_TIME).item(0);
89         document.getDocumentElement().removeChild(eventTimeNode);
90     }
91
92     private static boolean isSupported(XmlElement filter) {
93         return "subtree".equals(filter.getAttribute("type"))||
94                 "subtree".equals(filter.getAttribute("type", XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0));
95     }
96
97     private static Document extractNotificationContent(Document notification) throws DocumentedException {
98         XmlElement root = XmlElement.fromDomElement(notification.getDocumentElement());
99         XmlElement content = root.getOnlyChildElement();
100         notification.removeChild(root.getDomElement());
101         notification.appendChild(content.getDomElement());
102         return notification;
103     }
104
105     private static Document filteredNotification(XmlElement filter, Document originalNotification) throws DocumentedException {
106         Document result = XmlUtil.newDocument();
107         XmlElement dataSrc = XmlElement.fromDomDocument(originalNotification);
108         Element dataDst = (Element) result.importNode(dataSrc.getDomElement(), false);
109         for (XmlElement filterChild : filter.getChildElements()) {
110             addSubtree2(filterChild, dataSrc.getOnlyChildElement(), XmlElement.fromDomElement(dataDst));
111         }
112         if(dataDst.getFirstChild() != null) {
113             result.appendChild(dataDst.getFirstChild());
114             return result;
115         } else {
116             return null;
117         }
118     }
119
120     private static Document filtered(XmlElement filter, Document originalReplyDocument) throws DocumentedException {
121         Document result = XmlUtil.newDocument();
122         // even if filter is empty, copy /rpc/data
123         Element rpcReply = originalReplyDocument.getDocumentElement();
124         Node rpcReplyDst = result.importNode(rpcReply, false);
125         result.appendChild(rpcReplyDst);
126         XmlElement dataSrc = XmlElement.fromDomElement(rpcReply).getOnlyChildElement("data", XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0);
127         Element dataDst = (Element) result.importNode(dataSrc.getDomElement(), false);
128         rpcReplyDst.appendChild(dataDst);
129         addSubtree(filter, dataSrc, XmlElement.fromDomElement(dataDst));
130
131         return result;
132     }
133
134     private static void addSubtree(XmlElement filter, XmlElement src, XmlElement dst) throws DocumentedException {
135         for (XmlElement srcChild : src.getChildElements()) {
136             for (XmlElement filterChild : filter.getChildElements()) {
137                 addSubtree2(filterChild, srcChild, dst);
138             }
139         }
140     }
141
142     private static MatchingResult addSubtree2(XmlElement filter, XmlElement src, XmlElement dstParent) throws DocumentedException {
143         Document document = dstParent.getDomElement().getOwnerDocument();
144         MatchingResult matches = matches(src, filter);
145         if (matches != MatchingResult.NO_MATCH && matches != MatchingResult.CONTENT_MISMATCH) {
146             // copy srcChild to dst
147             boolean filterHasChildren = filter.getChildElements().isEmpty() == false;
148             // copy to depth if this is leaf of filter tree
149             Element copied = (Element) document.importNode(src.getDomElement(), filterHasChildren == false);
150             boolean shouldAppend = filterHasChildren == false;
151             if (filterHasChildren) { // this implies TAG_MATCH
152                 // do the same recursively
153                 int numberOfTextMatchingChildren = 0;
154                 for (XmlElement srcChild : src.getChildElements()) {
155                     for (XmlElement filterChild : filter.getChildElements()) {
156                         MatchingResult childMatch = addSubtree2(filterChild, srcChild, XmlElement.fromDomElement(copied));
157                         if (childMatch == MatchingResult.CONTENT_MISMATCH) {
158                             return MatchingResult.NO_MATCH;
159                         }
160                         if (childMatch == MatchingResult.CONTENT_MATCH) {
161                             numberOfTextMatchingChildren++;
162                         }
163                         shouldAppend |= childMatch != MatchingResult.NO_MATCH;
164                     }
165                 }
166                 // if only text matching child filters are specified..
167                 if (numberOfTextMatchingChildren == filter.getChildElements().size()) {
168                     // force all children to be added (to depth). This is done by copying parent node to depth.
169                     // implies shouldAppend == true
170                     copied = (Element) document.importNode(src.getDomElement(), true);
171                 }
172             }
173             if (shouldAppend) {
174                 dstParent.getDomElement().appendChild(copied);
175             }
176         }
177         return matches;
178     }
179
180     /**
181      * Shallow compare src node to filter: tag name and namespace must match.
182      * If filter node has no children and has text content, it also must match.
183      */
184     private static MatchingResult matches(XmlElement src, XmlElement filter) throws DocumentedException {
185         boolean tagMatch = src.getName().equals(filter.getName()) &&
186                 src.getNamespaceOptionally().equals(filter.getNamespaceOptionally());
187         MatchingResult result = null;
188         if (tagMatch) {
189             // match text content
190             Optional<String> maybeText = filter.getOnlyTextContentOptionally();
191             if (maybeText.isPresent()) {
192                 if (maybeText.equals(src.getOnlyTextContentOptionally()) || prefixedContentMatches(filter, src)) {
193                     result = MatchingResult.CONTENT_MATCH;
194                 } else {
195                     result = MatchingResult.CONTENT_MISMATCH;
196                 }
197             }
198             // match attributes, combination of content and tag is not supported
199             if (result == null) {
200                 for (Attr attr : filter.getAttributes().values()) {
201                     // ignore namespace declarations
202                     if (XmlUtil.XMLNS_URI.equals(attr.getNamespaceURI()) == false ) {
203                         // find attr with matching localName(),  namespaceURI(),  == value() in src
204                         String found = src.getAttribute(attr.getLocalName(), attr.getNamespaceURI());
205                         if (attr.getValue().equals(found) && result != MatchingResult.NO_MATCH) {
206                             result = MatchingResult.TAG_MATCH;
207                         } else {
208                             result = MatchingResult.NO_MATCH;
209                         }
210                     }
211                 }
212             }
213             if (result == null) {
214                 result = MatchingResult.TAG_MATCH;
215             }
216         }
217         if (result == null) {
218             result = MatchingResult.NO_MATCH;
219         }
220         LOG.debug("Matching {} to {} resulted in {}", src, filter, result);
221         return result;
222     }
223
224     private static boolean prefixedContentMatches(final XmlElement filter, final XmlElement src) throws DocumentedException {
225         final Map.Entry<String, String> prefixToNamespaceOfFilter;
226         final Map.Entry<String, String> prefixToNamespaceOfSrc;
227         try {
228             prefixToNamespaceOfFilter = filter.findNamespaceOfTextContent();
229             prefixToNamespaceOfSrc = src.findNamespaceOfTextContent();
230         } catch (IllegalArgumentException e) {
231             //if we can't find namespace of prefix - it's not a prefix, so it doesn't match
232             return false;
233         }
234
235         final String prefix = prefixToNamespaceOfFilter.getKey();
236         // If this is not a prefixed content, we do not need to continue since content do not match
237         if (prefix.equals(XmlElement.DEFAULT_NAMESPACE_PREFIX)) {
238             return false;
239         }
240         // Namespace mismatch
241         if (!prefixToNamespaceOfFilter.getValue().equals(prefixToNamespaceOfSrc.getValue())) {
242             return false;
243         }
244
245         final String unprefixedFilterContent = filter.getTextContent().substring(prefixToNamespaceOfFilter.getKey().length() + 1);
246         final String unprefixedSrcContnet = src.getTextContent().substring(prefixToNamespaceOfSrc.getKey().length() + 1);
247         // Finally compare unprefixed content
248         return unprefixedFilterContent.equals(unprefixedSrcContnet);
249     }
250
251     enum MatchingResult {
252         NO_MATCH, TAG_MATCH, CONTENT_MATCH, CONTENT_MISMATCH
253     }
254 }