2 * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.netconf.server.spi;
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.NamespaceURN;
16 import org.opendaylight.netconf.api.xml.XmlElement;
17 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
18 import org.opendaylight.netconf.api.xml.XmlUtil;
19 import org.opendaylight.netconf.server.api.operations.AbstractNetconfOperation.OperationNameAndNamespace;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import org.w3c.dom.Attr;
23 import org.w3c.dom.Document;
24 import org.w3c.dom.Element;
25 import org.w3c.dom.Node;
28 * See <a href="http://tools.ietf.org/html/rfc6241#section-6">rfc6241</a> for details.
30 public final class SubtreeFilter {
31 private static final Logger LOG = LoggerFactory.getLogger(SubtreeFilter.class);
33 private SubtreeFilter() {
37 public static Document applyRpcSubtreeFilter(final Document requestDocument,
38 final Document rpcReply) throws DocumentedException {
39 OperationNameAndNamespace operationNameAndNamespace = new OperationNameAndNamespace(requestDocument);
40 if (NamespaceURN.BASE.equals(operationNameAndNamespace.getNamespace())
41 && XmlNetconfConstants.GET.equals(operationNameAndNamespace.getOperationName())
42 || XmlNetconfConstants.GET_CONFIG.equals(operationNameAndNamespace.getOperationName())) {
43 // process subtree filtering here, in case registered netconf operations do
44 // not implement filtering.
45 Optional<XmlElement> maybeFilter = operationNameAndNamespace.getOperationElement()
46 .getOnlyChildElementOptionally(XmlNetconfConstants.FILTER, NamespaceURN.BASE);
47 if (maybeFilter.isEmpty()) {
50 XmlElement filter = maybeFilter.orElseThrow();
51 if (isSupported(filter)) {
54 return filtered(filter, rpcReply);
58 return rpcReply; // return identical document
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
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));
75 return Optional.of(extractNotificationContent(notification));
78 private static void removeEventTimeNode(final Document document) {
79 final Node eventTimeNode = document.getDocumentElement()
80 .getElementsByTagNameNS(NamespaceURN.NOTIFICATION, XmlNetconfConstants.EVENT_TIME)
82 document.getDocumentElement().removeChild(eventTimeNode);
85 private static boolean isSupported(final XmlElement filter) {
86 return "subtree".equals(filter.getAttribute("type"))
87 || "subtree".equals(filter.getAttribute("type", NamespaceURN.BASE));
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());
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));
106 if (dataDst.getFirstChild() != null) {
107 result.appendChild(dataDst.getFirstChild());
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", NamespaceURN.BASE);
121 Element dataDst = (Element) result.importNode(dataSrc.getDomElement(), false);
122 rpcReplyDst.appendChild(dataDst);
123 addSubtree(filter, dataSrc, XmlElement.fromDomElement(dataDst));
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);
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;
157 if (childMatch == MatchingResult.CONTENT_MATCH) {
158 numberOfTextMatchingChildren++;
160 shouldAppend |= childMatch != MatchingResult.NO_MATCH;
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);
171 dstParent.getDomElement().appendChild(copied);
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.
181 private static MatchingResult matches(final XmlElement src, final XmlElement filter) throws DocumentedException {
182 MatchingResult result = null;
183 if (src.getName().equals(filter.getName()) && Objects.equals(src.namespace(), filter.namespace())) {
184 // match text content
185 Optional<String> maybeText = filter.getOnlyTextContentOptionally();
186 if (maybeText.isPresent()) {
187 if (maybeText.equals(src.getOnlyTextContentOptionally()) || prefixedContentMatches(filter, src)) {
188 result = MatchingResult.CONTENT_MATCH;
190 result = MatchingResult.CONTENT_MISMATCH;
193 // match attributes, combination of content and tag is not supported
194 if (result == null) {
195 for (Attr attr : filter.getAttributes().values()) {
196 // ignore namespace declarations
197 if (!XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attr.getNamespaceURI())) {
198 // find attr with matching localName(), namespaceURI(), == value() in src
199 String found = src.getAttribute(attr.getLocalName(), attr.getNamespaceURI());
200 if (attr.getValue().equals(found) && result != MatchingResult.NO_MATCH) {
201 result = MatchingResult.TAG_MATCH;
203 result = MatchingResult.NO_MATCH;
208 if (result == null) {
209 result = MatchingResult.TAG_MATCH;
212 if (result == null) {
213 result = MatchingResult.NO_MATCH;
215 LOG.debug("Matching {} to {} resulted in {}", src, filter, result);
219 private static boolean prefixedContentMatches(final XmlElement filter,
220 final XmlElement src) throws DocumentedException {
221 final Map.Entry<String, String> prefixToNamespaceOfFilter;
222 final Map.Entry<String, String> prefixToNamespaceOfSrc;
224 prefixToNamespaceOfFilter = filter.findNamespaceOfTextContent();
225 prefixToNamespaceOfSrc = src.findNamespaceOfTextContent();
226 } catch (IllegalArgumentException e) {
227 //if we can't find namespace of prefix - it's not a prefix, so it doesn't match
231 final String prefix = prefixToNamespaceOfFilter.getKey();
232 // If this is not a prefixed content, we do not need to continue since content do not match
233 if (prefix.equals(XmlElement.DEFAULT_NAMESPACE_PREFIX)) {
236 // Namespace mismatch
237 if (!prefixToNamespaceOfFilter.getValue().equals(prefixToNamespaceOfSrc.getValue())) {
241 final String unprefixedFilterContent =
242 filter.getTextContent().substring(prefixToNamespaceOfFilter.getKey().length() + 1);
243 final String unprefixedSrcContnet =
244 src.getTextContent().substring(prefixToNamespaceOfSrc.getKey().length() + 1);
245 // Finally compare unprefixed content
246 return unprefixedFilterContent.equals(unprefixedSrcContnet);
249 enum MatchingResult {
250 NO_MATCH, TAG_MATCH, CONTENT_MATCH, CONTENT_MISMATCH