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