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