Remove DocumentedException.ErrorTag
[netconf.git] / netconf / netconf-api / src / main / java / org / opendaylight / netconf / api / DocumentedException.java
1 /*
2  * Copyright (c) 2015 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.api;
9
10 import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.RPC_REPLY_KEY;
11 import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0;
12
13 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
14 import java.util.HashMap;
15 import java.util.Map;
16 import java.util.Map.Entry;
17 import javax.xml.parsers.DocumentBuilderFactory;
18 import javax.xml.parsers.ParserConfigurationException;
19 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
20 import org.opendaylight.yangtools.yang.common.ErrorTag;
21 import org.opendaylight.yangtools.yang.common.ErrorType;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24 import org.w3c.dom.Document;
25 import org.w3c.dom.Node;
26 import org.w3c.dom.NodeList;
27
28 /**
29  * Checked exception to communicate an error that needs to be sent to the
30  * netconf client.
31  */
32 // FIXME: NETCONF-793: implement YangNetconfErrorAware
33 public class DocumentedException extends Exception {
34
35     public static final String RPC_ERROR = "rpc-error";
36     public static final String ERROR_TYPE = "error-type";
37     public static final String ERROR_TAG = "error-tag";
38     public static final String ERROR_SEVERITY = "error-severity";
39     public static final String ERROR_APP_TAG = "error-app-tag";
40     public static final String ERROR_PATH = "error-path";
41     public static final String ERROR_MESSAGE = "error-message";
42     public static final String ERROR_INFO = "error-info";
43
44     // FIXME: This is an RFC6241 definition, remove it once we have yangtools-7.0.5
45     @Deprecated(forRemoval = true)
46     public static final ErrorTag MALFORMED_MESSAGE = new ErrorTag("malformed-message");
47
48     private static final long serialVersionUID = 1L;
49     private static final Logger LOG = LoggerFactory.getLogger(DocumentedException.class);
50     private static final DocumentBuilderFactory BUILDER_FACTORY;
51
52     static {
53         BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
54         try {
55             BUILDER_FACTORY.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
56             BUILDER_FACTORY.setFeature("http://xml.org/sax/features/external-general-entities", false);
57             BUILDER_FACTORY.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
58             BUILDER_FACTORY.setXIncludeAware(false);
59             BUILDER_FACTORY.setExpandEntityReferences(false);
60         } catch (final ParserConfigurationException e) {
61             throw new ExceptionInInitializerError(e);
62         }
63         BUILDER_FACTORY.setNamespaceAware(true);
64         BUILDER_FACTORY.setCoalescing(true);
65         BUILDER_FACTORY.setIgnoringElementContentWhitespace(true);
66         BUILDER_FACTORY.setIgnoringComments(true);
67     }
68
69     private final ErrorType errorType;
70     @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "FIXME: should not be necessary with yangtools-7.0.5")
71     private final ErrorTag errorTag;
72     private final ErrorSeverity errorSeverity;
73     private final Map<String, String> errorInfo;
74
75     public DocumentedException(final String message) {
76         this(message, ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
77     }
78
79     public DocumentedException(final String message, final Exception cause) {
80         this(message, cause, ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
81     }
82
83     public DocumentedException(final String message, final ErrorType errorType, final ErrorTag errorTag,
84             final ErrorSeverity errorSeverity) {
85         this(message, errorType, errorTag, errorSeverity, Map.of());
86     }
87
88     public DocumentedException(final String message, final ErrorType errorType, final ErrorTag errorTag,
89             final ErrorSeverity errorSeverity, final Map<String, String> errorInfo) {
90         super(message);
91         this.errorType = errorType;
92         this.errorTag = errorTag;
93         this.errorSeverity = errorSeverity;
94         this.errorInfo = errorInfo;
95     }
96
97     public DocumentedException(final String message, final Exception cause, final ErrorType errorType,
98             final ErrorTag errorTag, final ErrorSeverity errorSeverity) {
99         this(message, cause, errorType, errorTag, errorSeverity, Map.of());
100     }
101
102     public DocumentedException(final String message, final Exception cause, final ErrorType errorType,
103             final ErrorTag errorTag, final ErrorSeverity errorSeverity, final Map<String, String> errorInfo) {
104         super(message, cause);
105         this.errorType = errorType;
106         this.errorTag = errorTag;
107         this.errorSeverity = errorSeverity;
108         // FIXME: this contract (based on what fromXMLDocument does) is quite wrong. It ignores the XML realities of
109         //        what constitutes a tag and especially tag value when faced with encoding XML-namespaced entities --
110         //        such as 'identity' arguments -- represented as QNames.
111         this.errorInfo = errorInfo;
112     }
113
114     public static DocumentedException wrap(final Exception exception) throws DocumentedException {
115         throw new DocumentedException(exception.getMessage(), exception, ErrorType.APPLICATION,
116                 ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR,
117                 Map.of(ErrorTag.OPERATION_FAILED.elementBody(), "Exception thrown"));
118     }
119
120     public static DocumentedException fromXMLDocument(final Document fromDoc) {
121
122         ErrorType errorType = ErrorType.APPLICATION;
123         ErrorTag errorTag = ErrorTag.OPERATION_FAILED;
124         ErrorSeverity errorSeverity = ErrorSeverity.ERROR;
125         Map<String, String> errorInfo = null;
126         String errorMessage = "";
127         String allErrorMessages = "";
128
129         Node rpcReply = fromDoc.getDocumentElement();
130
131         // FIXME: we only handle one rpc-error. For now, shove extra errorMessages found in multiple rpc-error in the
132         //        errorInfo Map to at least let them propagate back to caller.
133         //        this will be solved through migration to YangNetconfErrorAware, as that allows reporting multipl
134         //        error events
135         int rpcErrorCount = 0;
136
137         NodeList replyChildren = rpcReply.getChildNodes();
138         for (int i = 0; i < replyChildren.getLength(); i++) {
139             Node replyChild = replyChildren.item(i);
140             if (RPC_ERROR.equals(replyChild.getLocalName())) {
141                 rpcErrorCount++;
142                 NodeList rpcErrorChildren = replyChild.getChildNodes();
143                 for (int j = 0; j < rpcErrorChildren.getLength(); j++) {
144                     Node rpcErrorChild = rpcErrorChildren.item(j);
145
146                     // FIXME: use a switch expression here
147                     if (ERROR_TYPE.equals(rpcErrorChild.getLocalName())) {
148                         final ErrorType type = ErrorType.forElementBody(rpcErrorChild.getTextContent());
149                         // FIXME: this should be a hard error
150                         errorType = type != null ? type : ErrorType.APPLICATION;
151                     } else if (ERROR_TAG.equals(rpcErrorChild.getLocalName())) {
152                         errorTag = new ErrorTag(rpcErrorChild.getTextContent());
153                     } else if (ERROR_SEVERITY.equals(rpcErrorChild.getLocalName())) {
154                         final ErrorSeverity sev = ErrorSeverity.forElementBody(rpcErrorChild.getTextContent());
155                         // FIXME: this should be a hard error
156                         errorSeverity = sev != null ? sev : ErrorSeverity.ERROR;
157                     } else if (ERROR_MESSAGE.equals(rpcErrorChild.getLocalName())) {
158                         errorMessage = rpcErrorChild.getTextContent();
159                         allErrorMessages = allErrorMessages + errorMessage;
160                     } else if (ERROR_INFO.equals(rpcErrorChild.getLocalName())) {
161                         errorInfo = parseErrorInfo(rpcErrorChild);
162                     }
163                 }
164             }
165         }
166
167         if (rpcErrorCount > 1) {
168             if (errorInfo == null) {
169                 errorInfo = new HashMap<>();
170             }
171             errorInfo.put("Multiple Errors Found", allErrorMessages);
172         }
173
174         return new DocumentedException(errorMessage, errorType, errorTag, errorSeverity, errorInfo);
175     }
176
177     private static Map<String, String> parseErrorInfo(final Node node) {
178         Map<String, String> infoMap = new HashMap<>();
179         NodeList children = node.getChildNodes();
180         for (int i = 0; i < children.getLength(); i++) {
181             Node child = children.item(i);
182             if (child.getNodeType() == Node.ELEMENT_NODE) {
183                 // FIXME: Holy namespace ignorance, Batman!
184                 //
185                 // So this is just not enough to decode things in the general sense. getTextContenxt() may easily be a
186                 // qualified QName, such as an identity name or an instance-identifier. What the entire 'infoMap' needs
187                 // to contain is each child's XML context, so that the string literal can be interpreted as needed.
188                 //
189                 // yang.common.YangNamespaceContext represents the minimal API surface that needs to be exposed. That
190                 // effectively means:
191                 //
192                 // final class ElementValue implements YangNamespaceContext {
193                 //   public final String elementContenxt();
194                 // }
195                 //
196                 // Map<QName, ElementValue> infoMap;
197                 //
198                 // except... what do we use for revision?
199                 infoMap.put(child.getNodeName(), child.getTextContent());
200             }
201         }
202
203         return infoMap;
204     }
205
206     // FIXME: NETCONF-793: remove all of these in favor of YangNetconfErrorAware
207     public ErrorType getErrorType() {
208         return this.errorType;
209     }
210
211     public ErrorTag getErrorTag() {
212         return this.errorTag;
213     }
214
215     public ErrorSeverity getErrorSeverity() {
216         return this.errorSeverity;
217     }
218
219     public Map<String, String> getErrorInfo() {
220         return this.errorInfo;
221     }
222
223     // FIXME: this really should be an spi/util method (or even netconf-util-w3c-dom?) as this certainly is not the
224     //        primary interface we want to expose -- it is inherently mutable and its API is a pure nightmare.
225     public Document toXMLDocument() {
226         Document doc = null;
227         try {
228             doc = BUILDER_FACTORY.newDocumentBuilder().newDocument();
229
230             Node rpcReply = doc.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, RPC_REPLY_KEY);
231             doc.appendChild(rpcReply);
232
233             Node rpcError = doc.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, RPC_ERROR);
234             rpcReply.appendChild(rpcError);
235
236             rpcError.appendChild(createTextNode(doc, ERROR_TYPE, getErrorType().elementBody()));
237             rpcError.appendChild(createTextNode(doc, ERROR_TAG, getErrorTag().elementBody()));
238             rpcError.appendChild(createTextNode(doc, ERROR_SEVERITY, getErrorSeverity().elementBody()));
239             rpcError.appendChild(createTextNode(doc, ERROR_MESSAGE, getLocalizedMessage()));
240
241             Map<String, String> errorInfoMap = getErrorInfo();
242             if (errorInfoMap != null && !errorInfoMap.isEmpty()) {
243                 /*
244                  * <error-info> <bad-attribute>message-id</bad-attribute>
245                  * <bad-element>rpc</bad-element> </error-info>
246                  */
247
248                 Node errorInfoNode = doc.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, ERROR_INFO);
249                 errorInfoNode.setPrefix(rpcReply.getPrefix());
250                 rpcError.appendChild(errorInfoNode);
251
252                 for (Entry<String, String> entry : errorInfoMap.entrySet()) {
253                     errorInfoNode.appendChild(createTextNode(doc, entry.getKey(), entry.getValue()));
254                 }
255             }
256         } catch (final ParserConfigurationException e) {
257             // this shouldn't happen
258             LOG.error("Error outputting to XML document", e);
259         }
260
261         return doc;
262     }
263
264     private static Node createTextNode(final Document doc, final String tag, final String textContent) {
265         Node node = doc.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, tag);
266         node.setTextContent(textContent);
267         return node;
268     }
269
270     @Override
271     public String toString() {
272         return "NetconfDocumentedException{" + "message=" + getMessage() + ", errorType=" + this.errorType
273                 + ", errorTag=" + this.errorTag + ", errorSeverity=" + this.errorSeverity + ", errorInfo="
274                 + this.errorInfo + '}';
275     }
276 }