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