Merge "BUG-2288: DOMNotification API"
[controller.git] / opendaylight / md-sal / sal-rest-connector / src / main / java / org / opendaylight / controller / sal / rest / impl / RestconfDocumentedExceptionMapper.java
1 /*
2  * Copyright (c) 2014 Brocade Communications 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.controller.sal.rest.impl;
10
11 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERRORS_CONTAINER_QNAME;
12 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_APP_TAG_QNAME;
13 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_INFO_QNAME;
14 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_LIST_QNAME;
15 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_MESSAGE_QNAME;
16 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_TAG_QNAME;
17 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_TYPE_QNAME;
18 import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.NAMESPACE;
19 import com.google.common.base.Charsets;
20 import com.google.common.base.Strings;
21 import com.google.common.collect.ImmutableList;
22 import com.google.gson.stream.JsonWriter;
23 import java.io.ByteArrayOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.io.StringReader;
27 import java.io.UnsupportedEncodingException;
28 import java.util.List;
29 import java.util.Map.Entry;
30 import javax.activation.UnsupportedDataTypeException;
31 import javax.ws.rs.core.Context;
32 import javax.ws.rs.core.HttpHeaders;
33 import javax.ws.rs.core.MediaType;
34 import javax.ws.rs.core.Response;
35 import javax.ws.rs.ext.ExceptionMapper;
36 import javax.ws.rs.ext.Provider;
37 import javax.xml.parsers.DocumentBuilderFactory;
38 import javax.xml.transform.OutputKeys;
39 import javax.xml.transform.Transformer;
40 import javax.xml.transform.TransformerConfigurationException;
41 import javax.xml.transform.TransformerException;
42 import javax.xml.transform.TransformerFactory;
43 import javax.xml.transform.TransformerFactoryConfigurationError;
44 import javax.xml.transform.dom.DOMSource;
45 import javax.xml.transform.stream.StreamResult;
46 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
47 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
48 import org.opendaylight.controller.sal.restconf.impl.RestconfError;
49 import org.opendaylight.yangtools.yang.common.QName;
50 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
51 import org.opendaylight.yangtools.yang.data.api.Node;
52 import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode;
53 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils;
54 import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
55 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58 import org.w3c.dom.Document;
59 import org.xml.sax.InputSource;
60
61 /**
62  * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
63  * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
64  *
65  * @author Thomas Pantelis
66  */
67 @Provider
68 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
69
70     private final static Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
71     private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
72
73     @Context
74     private HttpHeaders headers;
75
76     @Override
77     public Response toResponse(final RestconfDocumentedException exception) {
78
79         LOG.debug("In toResponse: {}", exception.getMessage());
80
81
82
83         List<MediaType> accepts = headers.getAcceptableMediaTypes();
84         accepts.remove(MediaType.WILDCARD_TYPE);
85
86         LOG.debug("Accept headers: {}", accepts);
87
88         final MediaType mediaType;
89         if (accepts != null && accepts.size() > 0) {
90             mediaType = accepts.get(0); // just pick the first one
91         } else {
92             // Default to the content type if there's no Accept header
93             mediaType = MediaType.APPLICATION_JSON_TYPE;
94         }
95
96         LOG.debug("Using MediaType: {}", mediaType);
97
98         List<RestconfError> errors = exception.getErrors();
99         if (errors.isEmpty()) {
100             // We don't actually want to send any content but, if we don't set any content here,
101             // the tomcat front-end will send back an html error report. To prevent that, set a
102             // single space char in the entity.
103
104             return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
105         }
106
107         int status = errors.iterator().next().getErrorTag().getStatusCode();
108
109         ControllerContext context = ControllerContext.getInstance();
110         DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode();
111
112         if (errorsSchemaNode == null) {
113             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
114         }
115
116         ImmutableList.Builder<Node<?>> errorNodes = ImmutableList.<Node<?>> builder();
117         for (RestconfError error : errors) {
118             errorNodes.add(toDomNode(error));
119         }
120
121         ImmutableCompositeNode errorsNode = ImmutableCompositeNode.create(ERRORS_CONTAINER_QNAME, errorNodes.build());
122
123         Object responseBody;
124         if (mediaType.getSubtype().endsWith("json")) {
125             responseBody = toJsonResponseBody(errorsNode, errorsSchemaNode);
126         } else {
127             responseBody = toXMLResponseBody(errorsNode, errorsSchemaNode);
128         }
129
130         return Response.status(status).type(mediaType).entity(responseBody).build();
131     }
132
133     private Object toJsonResponseBody(final ImmutableCompositeNode errorsNode, final DataNodeContainer errorsSchemaNode) {
134
135         JsonMapper jsonMapper = new JsonMapper(null);
136
137         Object responseBody = null;
138         try {
139             ByteArrayOutputStream outStream = new ByteArrayOutputStream();
140             JsonWriter writer = new JsonWriter(new OutputStreamWriter(outStream, Charsets.UTF_8));
141             writer.setIndent("    ");
142
143             jsonMapper.write(writer, errorsNode, errorsSchemaNode);
144             writer.flush();
145
146             responseBody = outStream.toString("UTF-8");
147         } catch (IOException e) {
148             LOG.error("Error writing error response body", e);
149         }
150
151         return responseBody;
152     }
153
154     private Object toXMLResponseBody(final ImmutableCompositeNode errorsNode, final DataNodeContainer errorsSchemaNode) {
155
156         XmlMapper xmlMapper = new XmlMapper();
157
158         Object responseBody = null;
159         try {
160             Document xmlDoc = xmlMapper.write(errorsNode, errorsSchemaNode);
161
162             responseBody = documentToString(xmlDoc);
163         } catch (TransformerException | UnsupportedDataTypeException | UnsupportedEncodingException e) {
164             LOG.error("Error writing error response body", e);
165         }
166
167         return responseBody;
168     }
169
170     private String documentToString(final Document doc) throws TransformerException, UnsupportedEncodingException {
171         Transformer transformer = createTransformer();
172         ByteArrayOutputStream outStream = new ByteArrayOutputStream();
173
174         transformer.transform(new DOMSource(doc), new StreamResult(outStream));
175
176         return outStream.toString("UTF-8");
177     }
178
179     private Transformer createTransformer() throws TransformerFactoryConfigurationError,
180             TransformerConfigurationException {
181         Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
182         transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
183         transformer.setOutputProperty(OutputKeys.METHOD, "xml");
184         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
185         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
186         transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
187         return transformer;
188     }
189
190     private Node<?> toDomNode(final RestconfError error) {
191
192         CompositeNodeBuilder<ImmutableCompositeNode> builder = ImmutableCompositeNode.builder();
193         builder.setQName(ERROR_LIST_QNAME);
194
195         addLeaf(builder, ERROR_TYPE_QNAME, error.getErrorType().getErrorTypeTag());
196         addLeaf(builder, ERROR_TAG_QNAME, error.getErrorTag().getTagValue());
197         addLeaf(builder, ERROR_MESSAGE_QNAME, error.getErrorMessage());
198         addLeaf(builder, ERROR_APP_TAG_QNAME, error.getErrorAppTag());
199
200         Node<?> errorInfoNode = parseErrorInfo(error.getErrorInfo());
201         if (errorInfoNode != null) {
202             builder.add(errorInfoNode);
203         }
204
205         return builder.toInstance();
206     }
207
208     private Node<?> parseErrorInfo(final String errorInfo) {
209         if (Strings.isNullOrEmpty(errorInfo)) {
210             return null;
211         }
212
213         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
214
215         factory.setNamespaceAware(true);
216         factory.setCoalescing(true);
217         factory.setIgnoringElementContentWhitespace(true);
218         factory.setIgnoringComments(true);
219
220         // Wrap the error info content in a root <error-info> element so it can be parsed
221         // as XML. The error info content may or may not be XML. If not then it will be
222         // parsed as text content of the <error-info> element.
223
224         String errorInfoWithRoot = new StringBuilder("<error-info xmlns=\"").append(NAMESPACE).append("\">")
225                 .append(errorInfo).append("</error-info>").toString();
226
227         Document doc = null;
228         try {
229             doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(errorInfoWithRoot)));
230         } catch (Exception e) {
231             // TODO: what if the content is text that happens to contain invalid markup?
232             // Could wrap in CDATA and try again.
233
234             LOG.warn("Error parsing restconf error-info, \"{}\", as XML", errorInfo, e);
235             return null;
236         }
237
238         Node<?> errorInfoNode = XmlDocumentUtils.toDomNode(doc);
239
240         if (errorInfoNode instanceof CompositeNode) {
241             CompositeNode compositeNode = (CompositeNode) XmlDocumentUtils.toDomNode(doc);
242
243             // At this point the QName for the "error-info" CompositeNode doesn't contain the revision
244             // as it isn't present in the XML. So we'll copy all the child nodes and create a new
245             // CompositeNode with the full QName. This is done so the XML/JSON mapping code can
246             // locate the schema.
247
248             ImmutableList.Builder<Node<?>> childNodes = ImmutableList.builder();
249             for (Entry<QName, List<Node<?>>> entry : compositeNode.entrySet()) {
250                 childNodes.addAll(entry.getValue());
251             }
252
253             errorInfoNode = ImmutableCompositeNode.create(ERROR_INFO_QNAME, childNodes.build());
254         }
255
256         return errorInfoNode;
257     }
258
259     private void addLeaf(final CompositeNodeBuilder<ImmutableCompositeNode> builder, final QName qname,
260             final String value) {
261         if (!Strings.isNullOrEmpty(value)) {
262             builder.addLeaf(qname, value);
263         }
264     }
265 }