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