aeeaedf601a04a2fe305a575420e4f4fd0cdce3d
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / streams / listeners / EventFormatter.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. 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.restconf.nb.rfc8040.streams.listeners;
9
10 import java.io.IOException;
11 import java.time.Instant;
12 import java.time.OffsetDateTime;
13 import java.time.ZoneId;
14 import java.time.format.DateTimeFormatter;
15 import java.util.Optional;
16 import javax.xml.XMLConstants;
17 import javax.xml.parsers.DocumentBuilderFactory;
18 import javax.xml.parsers.ParserConfigurationException;
19 import javax.xml.xpath.XPath;
20 import javax.xml.xpath.XPathConstants;
21 import javax.xml.xpath.XPathExpression;
22 import javax.xml.xpath.XPathExpressionException;
23 import javax.xml.xpath.XPathFactory;
24 import org.eclipse.jdt.annotation.NonNull;
25 import org.opendaylight.yangtools.concepts.Immutable;
26 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
27 import org.w3c.dom.Document;
28
29 abstract class EventFormatter<T> implements Immutable {
30     private static final XPathFactory XPF = XPathFactory.newInstance();
31
32     // FIXME: NETCONF-369: XPath operates without namespace context, therefore we need an namespace-unaware builder.
33     //        Once it is fixed we can use UntrustedXML instead.
34     private static final @NonNull DocumentBuilderFactory DBF;
35
36     static {
37         final DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
38         f.setCoalescing(true);
39         f.setExpandEntityReferences(false);
40         f.setIgnoringElementContentWhitespace(true);
41         f.setIgnoringComments(true);
42         f.setXIncludeAware(false);
43         try {
44             f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
45             f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
46             f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
47             f.setFeature("http://xml.org/sax/features/external-general-entities", false);
48             f.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
49         } catch (final ParserConfigurationException e) {
50             throw new ExceptionInInitializerError(e);
51         }
52         DBF = f;
53     }
54
55     private final XPathExpression filter;
56
57     EventFormatter()  {
58         this.filter = null;
59     }
60
61     EventFormatter(final String xpathFilter)  throws XPathExpressionException {
62         final XPath xpath;
63         synchronized (XPF) {
64             xpath = XPF.newXPath();
65         }
66         // FIXME: NETCONF-369: we need to bind the namespace context here and for that we need the SchemaContext
67         filter = xpath.compile(xpathFilter);
68     }
69
70     final Optional<String> eventData(final EffectiveModelContext schemaContext, final T input, final Instant now,
71                                      final boolean leafNodesOnly, final boolean skipData,
72                                      final boolean changedLeafNodesOnly)
73             throws Exception {
74         if (!filterMatches(schemaContext, input, now)) {
75             return Optional.empty();
76         }
77         return Optional.ofNullable(
78                 createText(schemaContext, input, now, leafNodesOnly, skipData, changedLeafNodesOnly));
79     }
80
81     /**
82      * Export the provided input into the provided document so we can verify whether a filter matches the content.
83      *
84      * @param doc the document to fill
85      * @param schemaContext context to use for the export
86      * @param input data to export
87      * @throws IOException if any IOException occurs during export to the document
88      */
89     abstract void fillDocument(Document doc, EffectiveModelContext schemaContext, T input) throws IOException;
90
91     /**
92      * Format the input data into string representation of the data provided.
93      *
94      * @param schemaContext context to use for the export
95      * @param input input data
96      * @param now time the event happened
97      * @param leafNodesOnly option to include only leaves in the result
98      * @param skipData option to skip data in the result, only paths would be included
99      * @param changedLeafNodesOnly  option to include only changed leaves in the result
100      * @return String representation of the formatted data
101      * @throws Exception if the underlying formatters fail to export the data to the requested format
102      */
103     abstract String createText(EffectiveModelContext schemaContext, T input, Instant now, boolean leafNodesOnly,
104                                boolean skipData, boolean changedLeafNodesOnly) throws Exception;
105
106     private boolean filterMatches(final EffectiveModelContext schemaContext, final T input, final Instant now)
107             throws IOException {
108         if (filter == null) {
109             return true;
110         }
111
112         final Document doc;
113         try {
114             doc = DBF.newDocumentBuilder().newDocument();
115         } catch (final ParserConfigurationException e) {
116             throw new IOException("Failed to create a new document", e);
117         }
118         fillDocument(doc, schemaContext, input);
119
120         final Boolean eval;
121         try {
122             eval = (Boolean) filter.evaluate(doc, XPathConstants.BOOLEAN);
123         } catch (final XPathExpressionException e) {
124             throw new IllegalStateException("Failed to evaluate expression " + filter, e);
125         }
126
127         return eval;
128     }
129
130     /**
131      * Formats data specified by RFC3339.
132      *
133      * @param now time stamp
134      * @return Data specified by RFC3339.
135      */
136     static String toRFC3339(final Instant now) {
137         return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(OffsetDateTime.ofInstant(now, ZoneId.systemDefault()));
138     }
139 }