2 * Copyright (c) 2014 Brocade Communications 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
9 package org.opendaylight.controller.sal.rest.impl;
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;
20 import com.google.common.base.Charsets;
21 import com.google.common.base.Strings;
22 import com.google.common.collect.ImmutableList;
23 import com.google.gson.stream.JsonWriter;
24 import java.io.ByteArrayOutputStream;
25 import java.io.IOException;
26 import java.io.OutputStreamWriter;
27 import java.io.StringReader;
28 import java.io.UnsupportedEncodingException;
29 import java.util.List;
30 import java.util.Map.Entry;
31 import javax.activation.UnsupportedDataTypeException;
32 import javax.ws.rs.core.Context;
33 import javax.ws.rs.core.HttpHeaders;
34 import javax.ws.rs.core.MediaType;
35 import javax.ws.rs.core.Response;
36 import javax.ws.rs.ext.ExceptionMapper;
37 import javax.ws.rs.ext.Provider;
38 import javax.xml.parsers.DocumentBuilderFactory;
39 import javax.xml.transform.OutputKeys;
40 import javax.xml.transform.Transformer;
41 import javax.xml.transform.TransformerConfigurationException;
42 import javax.xml.transform.TransformerException;
43 import javax.xml.transform.TransformerFactory;
44 import javax.xml.transform.TransformerFactoryConfigurationError;
45 import javax.xml.transform.dom.DOMSource;
46 import javax.xml.transform.stream.StreamResult;
47 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
48 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
49 import org.opendaylight.controller.sal.restconf.impl.RestconfError;
50 import org.opendaylight.yangtools.yang.common.QName;
51 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
52 import org.opendaylight.yangtools.yang.data.api.Node;
53 import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode;
54 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils;
55 import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
56 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59 import org.w3c.dom.Document;
60 import org.xml.sax.InputSource;
63 * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
64 * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
66 * @author Thomas Pantelis
69 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
71 private final static Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
74 private HttpHeaders headers;
77 public Response toResponse(final RestconfDocumentedException exception) {
79 LOG.debug("In toResponse: {}", exception.getMessage());
81 // Default to the content type if there's no Accept header
83 MediaType mediaType = headers.getMediaType();
85 List<MediaType> accepts = headers.getAcceptableMediaTypes();
87 LOG.debug("Accept headers: {}", accepts);
89 if (accepts != null && accepts.size() > 0) {
90 mediaType = accepts.get(0); // just pick the first one
93 LOG.debug("Using MediaType: {}", mediaType);
95 List<RestconfError> errors = exception.getErrors();
96 if (errors.isEmpty()) {
97 // We don't actually want to send any content but, if we don't set any content here,
98 // the tomcat front-end will send back an html error report. To prevent that, set a
99 // single space char in the entity.
101 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
104 int status = errors.iterator().next().getErrorTag().getStatusCode();
106 ControllerContext context = ControllerContext.getInstance();
107 DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode();
109 if (errorsSchemaNode == null) {
110 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
113 ImmutableList.Builder<Node<?>> errorNodes = ImmutableList.<Node<?>> builder();
114 for (RestconfError error : errors) {
115 errorNodes.add(toDomNode(error));
118 ImmutableCompositeNode errorsNode = ImmutableCompositeNode.create(ERRORS_CONTAINER_QNAME, errorNodes.build());
121 if (mediaType.getSubtype().endsWith("json")) {
122 responseBody = toJsonResponseBody(errorsNode, errorsSchemaNode);
124 responseBody = toXMLResponseBody(errorsNode, errorsSchemaNode);
127 return Response.status(status).type(mediaType).entity(responseBody).build();
130 private Object toJsonResponseBody(final ImmutableCompositeNode errorsNode, final DataNodeContainer errorsSchemaNode) {
132 JsonMapper jsonMapper = new JsonMapper(null);
134 Object responseBody = null;
136 ByteArrayOutputStream outStream = new ByteArrayOutputStream();
137 JsonWriter writer = new JsonWriter(new OutputStreamWriter(outStream, Charsets.UTF_8));
138 writer.setIndent(" ");
140 jsonMapper.write(writer, errorsNode, errorsSchemaNode);
143 responseBody = outStream.toString("UTF-8");
144 } catch (IOException e) {
145 LOG.error("Error writing error response body", e);
151 private Object toXMLResponseBody(final ImmutableCompositeNode errorsNode, final DataNodeContainer errorsSchemaNode) {
153 XmlMapper xmlMapper = new XmlMapper();
155 Object responseBody = null;
157 Document xmlDoc = xmlMapper.write(errorsNode, errorsSchemaNode);
159 responseBody = documentToString(xmlDoc);
160 } catch (TransformerException | UnsupportedDataTypeException | UnsupportedEncodingException e) {
161 LOG.error("Error writing error response body", e);
167 private String documentToString(final Document doc) throws TransformerException, UnsupportedEncodingException {
168 Transformer transformer = createTransformer();
169 ByteArrayOutputStream outStream = new ByteArrayOutputStream();
171 transformer.transform(new DOMSource(doc), new StreamResult(outStream));
173 return outStream.toString("UTF-8");
176 private Transformer createTransformer() throws TransformerFactoryConfigurationError,
177 TransformerConfigurationException {
178 TransformerFactory tf = TransformerFactory.newInstance();
179 Transformer transformer = tf.newTransformer();
180 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
181 transformer.setOutputProperty(OutputKeys.METHOD, "xml");
182 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
183 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
184 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
188 private Node<?> toDomNode(final RestconfError error) {
190 CompositeNodeBuilder<ImmutableCompositeNode> builder = ImmutableCompositeNode.builder();
191 builder.setQName(ERROR_LIST_QNAME);
193 addLeaf(builder, ERROR_TYPE_QNAME, error.getErrorType().getErrorTypeTag());
194 addLeaf(builder, ERROR_TAG_QNAME, error.getErrorTag().getTagValue());
195 addLeaf(builder, ERROR_MESSAGE_QNAME, error.getErrorMessage());
196 addLeaf(builder, ERROR_APP_TAG_QNAME, error.getErrorAppTag());
198 Node<?> errorInfoNode = parseErrorInfo(error.getErrorInfo());
199 if (errorInfoNode != null) {
200 builder.add(errorInfoNode);
203 return builder.toInstance();
206 private Node<?> parseErrorInfo(final String errorInfo) {
207 if (Strings.isNullOrEmpty(errorInfo)) {
211 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
212 factory.setNamespaceAware(true);
213 factory.setCoalescing(true);
214 factory.setIgnoringElementContentWhitespace(true);
215 factory.setIgnoringComments(true);
217 // Wrap the error info content in a root <error-info> element so it can be parsed
218 // as XML. The error info content may or may not be XML. If not then it will be
219 // parsed as text content of the <error-info> element.
221 String errorInfoWithRoot = new StringBuilder("<error-info xmlns=\"").append(NAMESPACE).append("\">")
222 .append(errorInfo).append("</error-info>").toString();
226 doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(errorInfoWithRoot)));
227 } catch (Exception e) {
228 // TODO: what if the content is text that happens to contain invalid markup?
229 // Could wrap in CDATA and try again.
231 LOG.warn("Error parsing restconf error-info, \"{}\", as XML", errorInfo, e);
235 Node<?> errorInfoNode = XmlDocumentUtils.toDomNode(doc);
237 if (errorInfoNode instanceof CompositeNode) {
238 CompositeNode compositeNode = (CompositeNode) XmlDocumentUtils.toDomNode(doc);
240 // At this point the QName for the "error-info" CompositeNode doesn't contain the revision
241 // as it isn't present in the XML. So we'll copy all the child nodes and create a new
242 // CompositeNode with the full QName. This is done so the XML/JSON mapping code can
243 // locate the schema.
245 ImmutableList.Builder<Node<?>> childNodes = ImmutableList.builder();
246 for (Entry<QName, List<Node<?>>> entry : compositeNode.entrySet()) {
247 childNodes.addAll(entry.getValue());
250 errorInfoNode = ImmutableCompositeNode.create(ERROR_INFO_QNAME, childNodes.build());
253 return errorInfoNode;
256 private void addLeaf(final CompositeNodeBuilder<ImmutableCompositeNode> builder, final QName qname,
257 final String value) {
258 if (!Strings.isNullOrEmpty(value)) {
259 builder.addLeaf(qname, value);