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 java.io.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStreamWriter;
23 import java.io.StringReader;
24 import java.io.UnsupportedEncodingException;
25 import java.util.List;
26 import java.util.Map.Entry;
28 import javax.activation.UnsupportedDataTypeException;
29 import javax.ws.rs.core.Context;
30 import javax.ws.rs.core.HttpHeaders;
31 import javax.ws.rs.core.MediaType;
32 import javax.ws.rs.core.Response;
33 import javax.ws.rs.ext.ExceptionMapper;
34 import javax.ws.rs.ext.Provider;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.transform.OutputKeys;
37 import javax.xml.transform.Transformer;
38 import javax.xml.transform.TransformerConfigurationException;
39 import javax.xml.transform.TransformerException;
40 import javax.xml.transform.TransformerFactory;
41 import javax.xml.transform.TransformerFactoryConfigurationError;
42 import javax.xml.transform.dom.DOMSource;
43 import javax.xml.transform.stream.StreamResult;
45 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
46 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
47 import org.opendaylight.controller.sal.restconf.impl.RestconfError;
48 import org.opendaylight.yangtools.yang.common.QName;
49 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
50 import org.opendaylight.yangtools.yang.data.api.Node;
51 import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode;
52 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils;
53 import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
54 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57 import org.w3c.dom.Document;
58 import org.xml.sax.InputSource;
60 import com.google.common.base.Strings;
61 import com.google.common.collect.ImmutableList;
62 import com.google.gson.stream.JsonWriter;
65 * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by
66 * resource implementations and translates appropriately to restconf error response as defined in
67 * the RESTCONF RFC draft.
69 * @author Thomas Pantelis
72 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
74 private final static Logger LOG = LoggerFactory.getLogger( RestconfDocumentedExceptionMapper.class );
77 private HttpHeaders headers;
80 public Response toResponse( final RestconfDocumentedException exception ) {
82 LOG.debug( "In toResponse: {}", exception.getMessage() );
84 // Default to the content type if there's no Accept header
86 MediaType mediaType = headers.getMediaType();
88 List<MediaType> accepts = headers.getAcceptableMediaTypes();
90 LOG.debug( "Accept headers: {}", accepts );
92 if( accepts != null && accepts.size() > 0 ) {
93 mediaType = accepts.get( 0 ); // just pick the first one
96 LOG.debug( "Using MediaType: {}", mediaType );
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.
104 return Response.status( exception.getStatus() )
105 .type( MediaType.TEXT_PLAIN_TYPE )
106 .entity( " " ).build();
109 int status = errors.iterator().next().getErrorTag().getStatusCode();
111 ControllerContext context = ControllerContext.getInstance();
112 DataNodeContainer errorsSchemaNode = (DataNodeContainer)context.getRestconfModuleErrorsSchemaNode();
114 if( errorsSchemaNode == null ) {
115 return Response.status( status )
116 .type( MediaType.TEXT_PLAIN_TYPE )
117 .entity( exception.getMessage() ).build();
120 ImmutableList.Builder<Node<?>> errorNodes = ImmutableList.<Node<?>> builder();
121 for( RestconfError error: errors ) {
122 errorNodes.add( toDomNode( error ) );
125 ImmutableCompositeNode errorsNode =
126 ImmutableCompositeNode.create( ERRORS_CONTAINER_QNAME, errorNodes.build() );
129 if( mediaType.getSubtype().endsWith( "json" ) ) {
130 responseBody = toJsonResponseBody( errorsNode, errorsSchemaNode );
133 responseBody = toXMLResponseBody( errorsNode, errorsSchemaNode );
136 return Response.status( status ).type( mediaType ).entity( responseBody ).build();
139 private Object toJsonResponseBody( final ImmutableCompositeNode errorsNode,
140 final DataNodeContainer errorsSchemaNode ) {
142 JsonMapper jsonMapper = new JsonMapper();
144 Object responseBody = null;
146 ByteArrayOutputStream outStream = new ByteArrayOutputStream();
147 JsonWriter writer = new JsonWriter( new OutputStreamWriter( outStream, "UTF-8" ) );
148 writer.setIndent( " " );
150 jsonMapper.write( writer, errorsNode, errorsSchemaNode, null );
153 responseBody = outStream.toString( "UTF-8" );
155 catch( IOException e ) {
156 LOG.error( "Error writing error response body", e );
162 private Object toXMLResponseBody( final ImmutableCompositeNode errorsNode,
163 final DataNodeContainer errorsSchemaNode ) {
165 XmlMapper xmlMapper = new XmlMapper();
167 Object responseBody = null;
169 Document xmlDoc = xmlMapper.write( errorsNode, errorsSchemaNode );
171 responseBody = documentToString( xmlDoc );
173 catch( TransformerException | UnsupportedDataTypeException | UnsupportedEncodingException e ) {
174 LOG.error( "Error writing error response body", e );
180 private String documentToString( final Document doc ) throws TransformerException, UnsupportedEncodingException {
181 Transformer transformer = createTransformer();
182 ByteArrayOutputStream outStream = new ByteArrayOutputStream();
184 transformer.transform( new DOMSource( doc ), new StreamResult( outStream ) );
186 return outStream.toString( "UTF-8" );
189 private Transformer createTransformer() throws TransformerFactoryConfigurationError,
190 TransformerConfigurationException {
191 TransformerFactory tf = TransformerFactory.newInstance();
192 Transformer transformer = tf.newTransformer();
193 transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "no" );
194 transformer.setOutputProperty( OutputKeys.METHOD, "xml" );
195 transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
196 transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" );
197 transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "4" );
201 private Node<?> toDomNode( final RestconfError error ) {
203 CompositeNodeBuilder<ImmutableCompositeNode> builder = ImmutableCompositeNode.builder();
204 builder.setQName( ERROR_LIST_QNAME );
206 addLeaf( builder, ERROR_TYPE_QNAME, error.getErrorType().getErrorTypeTag() );
207 addLeaf( builder, ERROR_TAG_QNAME, error.getErrorTag().getTagValue() );
208 addLeaf( builder, ERROR_MESSAGE_QNAME, error.getErrorMessage() );
209 addLeaf( builder, ERROR_APP_TAG_QNAME, error.getErrorAppTag() );
211 Node<?> errorInfoNode = parseErrorInfo( error.getErrorInfo() );
212 if( errorInfoNode != null ) {
213 builder.add( errorInfoNode );
216 return builder.toInstance();
219 private Node<?> parseErrorInfo( final String errorInfo ) {
220 if( Strings.isNullOrEmpty( errorInfo ) ) {
224 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
225 factory.setNamespaceAware( true );
226 factory.setCoalescing( true );
227 factory.setIgnoringElementContentWhitespace( true );
228 factory.setIgnoringComments( true );
230 // Wrap the error info content in a root <error-info> element so it can be parsed
231 // as XML. The error info content may or may not be XML. If not then it will be
232 // parsed as text content of the <error-info> element.
234 String errorInfoWithRoot =
235 new StringBuilder( "<error-info xmlns=\"" ).append( NAMESPACE ).append( "\">" )
236 .append( errorInfo ).append( "</error-info>" ).toString();
240 doc = factory.newDocumentBuilder().parse(
241 new InputSource( new StringReader( errorInfoWithRoot ) ) );
243 catch( Exception e ) {
244 // TODO: what if the content is text that happens to contain invalid markup? Could
245 // wrap in CDATA and try again.
247 LOG.warn( "Error parsing restconf error-info, \"" + errorInfo + "\", as XML: " +
252 Node<?> errorInfoNode = XmlDocumentUtils.toDomNode( doc );
254 if( errorInfoNode instanceof CompositeNode ) {
255 CompositeNode compositeNode = (CompositeNode)XmlDocumentUtils.toDomNode( doc );
257 // At this point the QName for the "error-info" CompositeNode doesn't contain the revision
258 // as it isn't present in the XML. So we'll copy all the child nodes and create a new
259 // CompositeNode with the full QName. This is done so the XML/JSON mapping code can
260 // locate the schema.
262 ImmutableList.Builder<Node<?>> childNodes = ImmutableList.builder();
263 for( Entry<QName, List<Node<?>>> entry: compositeNode.entrySet() ) {
264 childNodes.addAll( entry.getValue() );
267 errorInfoNode = ImmutableCompositeNode.create( ERROR_INFO_QNAME, childNodes.build() );
270 return errorInfoNode;
273 private void addLeaf( final CompositeNodeBuilder<ImmutableCompositeNode> builder, final QName qname,
274 final String value ) {
275 if( !Strings.isNullOrEmpty( value ) ) {
276 builder.addLeaf( qname, value );