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