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