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