22dabf42efbd20ebd8d2fba6ee5cc0d65a12b9ef
[netconf.git] / opendaylight / restconf / sal-rest-connector / src / main / java / org / opendaylight / netconf / 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.netconf.sal.rest.impl;
10
11 import com.google.common.base.Charsets;
12 import com.google.common.base.Preconditions;
13 import com.google.common.base.Throwables;
14 import com.google.common.collect.Iterables;
15 import com.google.gson.stream.JsonWriter;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.OutputStreamWriter;
19 import java.net.URI;
20 import java.util.Iterator;
21 import java.util.List;
22 import javax.ws.rs.core.Context;
23 import javax.ws.rs.core.HttpHeaders;
24 import javax.ws.rs.core.MediaType;
25 import javax.ws.rs.core.Response;
26 import javax.ws.rs.ext.ExceptionMapper;
27 import javax.ws.rs.ext.Provider;
28 import javax.xml.XMLConstants;
29 import javax.xml.stream.FactoryConfigurationError;
30 import javax.xml.stream.XMLOutputFactory;
31 import javax.xml.stream.XMLStreamException;
32 import javax.xml.stream.XMLStreamWriter;
33 import org.opendaylight.netconf.sal.rest.api.Draft02;
34 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
35 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
36 import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
37 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
38 import org.opendaylight.netconf.sal.restconf.impl.RestconfError;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
44 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
45 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
46 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
47 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
49 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
50 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
51 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
52 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
53 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
54 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter;
55 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
56 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
57 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder;
58 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder;
59 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
61 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
63 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
65 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
71  * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
72  *
73  * @author Thomas Pantelis
74  */
75 @Provider
76 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
77
78     private final static Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
79
80     private static final XMLOutputFactory XML_FACTORY;
81
82     static {
83         XML_FACTORY = XMLOutputFactory.newFactory();
84         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
85     }
86
87     @Context
88     private HttpHeaders headers;
89
90     @Override
91     public Response toResponse(final RestconfDocumentedException exception) {
92
93         LOG.debug("In toResponse: {}", exception.getMessage());
94
95         final List<MediaType> accepts = headers.getAcceptableMediaTypes();
96         accepts.remove(MediaType.WILDCARD_TYPE);
97
98         LOG.debug("Accept headers: {}", accepts);
99
100         final MediaType mediaType;
101         if (accepts != null && accepts.size() > 0) {
102             mediaType = accepts.get(0); // just pick the first one
103         } else {
104             // Default to the content type if there's no Accept header
105             mediaType = MediaType.APPLICATION_JSON_TYPE;
106         }
107
108         LOG.debug("Using MediaType: {}", mediaType);
109
110         final List<RestconfError> errors = exception.getErrors();
111         if (errors.isEmpty()) {
112             // We don't actually want to send any content but, if we don't set any content here,
113             // the tomcat front-end will send back an html error report. To prevent that, set a
114             // single space char in the entity.
115
116             return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
117         }
118
119         final int status = errors.iterator().next().getErrorTag().getStatusCode();
120
121         final ControllerContext context = ControllerContext.getInstance();
122         final DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode();
123
124         if (errorsSchemaNode == null) {
125             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
126         }
127
128         Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
129                 "Found Errors SchemaNode isn't ContainerNode");
130         final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> errContBuild =
131                 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
132
133         final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
134                 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
135         final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
136         Preconditions.checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
137         final CollectionNodeBuilder<MapEntryNode, MapNode> listErorsBuilder = Builders
138                 .mapBuilder((ListSchemaNode) errListSchemaNode);
139
140
141         for (final RestconfError error : errors) {
142             listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
143         }
144         errContBuild.withChild(listErorsBuilder.build());
145
146         final NormalizedNodeContext errContext =  new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
147                 (DataSchemaNode) errorsSchemaNode, null, context.getGlobalSchema()), errContBuild.build());
148
149         Object responseBody;
150         if (mediaType.getSubtype().endsWith("json")) {
151             responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
152         } else {
153             responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
154         }
155
156         return Response.status(status).type(mediaType).entity(responseBody).build();
157     }
158
159     private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
160         Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode,
161                 "errListSchemaNode has to be of type ListSchemaNode");
162         final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
163         final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = Builders
164                 .mapEntryBuilder(listStreamSchemaNode);
165
166         List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
167                 (listStreamSchemaNode), "error-type");
168         final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
169         Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode);
170         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
171                 .withValue(error.getErrorType().getErrorTypeTag()).build());
172
173         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
174                 (listStreamSchemaNode), "error-tag");
175         final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
176         Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode);
177         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
178                 .withValue(error.getErrorTag().getTagValue()).build());
179
180         if (error.getErrorAppTag() != null) {
181             lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
182                     (listStreamSchemaNode), "error-app-tag");
183             final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
184             Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
185             errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
186                     .withValue(error.getErrorAppTag()).build());
187         }
188
189         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
190                 (listStreamSchemaNode), "error-message");
191         final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
192         Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode);
193         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
194                 .withValue(error.getErrorMessage()).build());
195
196         if(error.getErrorInfo() != null) {
197             // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
198             // intention is for implementors to define their own data content so we'll just treat it as a leaf
199             // with string data.
200             errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
201                     error.getErrorInfo()));
202         }
203
204         // TODO : find how could we add possible "error-path"
205
206         return errNodeValues.build();
207     }
208
209     private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) {
210         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
211         NormalizedNode<?, ?> data = errorsNode.getData();
212         final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
213         final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
214
215         SchemaPath path = context.getSchemaNode().getPath();
216         final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, Charsets.UTF_8);
217         if (data == null) {
218             throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
219         }
220
221         boolean isDataRoot = false;
222         URI initialNs = null;
223         if (SchemaPath.ROOT.equals(path)) {
224             isDataRoot = true;
225         } else {
226             path = path.getParent();
227             // FIXME: Add proper handling of reading root.
228         }
229         if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
230             initialNs = schema.getQName().getNamespace();
231         }
232
233         final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
234         final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
235                 JSONCodecFactory.create(context.getSchemaContext()), path, initialNs, jsonWriter);
236
237         // We create a delegating writer to special-case error-info as error-info is defined as an empty
238         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
239         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
240         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
241         // for error-info.
242         final NormalizedNodeStreamWriter streamWriter = new DelegatingNormalizedNodeStreamWriter(jsonStreamWriter) {
243             @Override
244             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
245                 if(name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
246                     jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
247                     jsonWriter.value(value.toString());
248                 } else {
249                     super.leafNode(name, value);
250                 }
251             }
252         };
253
254         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
255         try {
256             if (isDataRoot) {
257                 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
258             } else {
259                 if (data instanceof MapEntryNode) {
260                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild(((MapEntryNode) data)).build();
261                 }
262                 nnWriter.write(data);
263             }
264             nnWriter.flush();
265             outputWriter.flush();
266         } catch (final IOException e) {
267             LOG.warn("Error writing error response body", e);
268         }
269
270         return outStream.toString();
271
272     }
273
274     private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) {
275         final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
276         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
277
278         final XMLStreamWriter xmlWriter;
279         try {
280             xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, "UTF-8");
281         } catch (final XMLStreamException e) {
282             throw new IllegalStateException(e);
283         } catch (final FactoryConfigurationError e) {
284             throw new IllegalStateException(e);
285         }
286         NormalizedNode<?, ?> data = errorsNode.getData();
287         SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
288
289         boolean isDataRoot = false;
290         if (SchemaPath.ROOT.equals(schemaPath)) {
291             isDataRoot = true;
292         } else {
293             schemaPath = schemaPath.getParent();
294         }
295
296         final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
297                 pathContext.getSchemaContext(), schemaPath);
298
299         // We create a delegating writer to special-case error-info as error-info is defined as an empty
300         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
301         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
302         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
303         // for error-info.
304         final NormalizedNodeStreamWriter streamWriter = new DelegatingNormalizedNodeStreamWriter(xmlStreamWriter) {
305             @Override
306             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
307                 if(name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
308                     String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
309                     try {
310                         xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
311                                 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
312                         xmlWriter.writeCharacters(value.toString());
313                         xmlWriter.writeEndElement();
314                     } catch (XMLStreamException e) {
315                         throw new IOException("Error writing error-info", e);
316                     }
317                 } else {
318                     super.leafNode(name, value);
319                 }
320             }
321         };
322
323         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
324         try {
325             if (isDataRoot) {
326                 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
327             } else {
328                 if (data instanceof MapEntryNode) {
329                     // Restconf allows returning one list item. We need to wrap it
330                     // in map node in order to serialize it properly
331                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
332                 }
333                 nnWriter.write(data);
334                 nnWriter.flush();
335             }
336         }
337         catch (final IOException e) {
338             LOG.warn("Error writing error response body.", e);
339         }
340
341         return outStream.toString();
342     }
343
344     private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data)
345             throws IOException {
346         try {
347             final QName name = SchemaContext.NAME;
348             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
349             for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
350                 nnWriter.write(child);
351             }
352             nnWriter.flush();
353             xmlWriter.writeEndElement();
354             xmlWriter.flush();
355         } catch (final XMLStreamException e) {
356             Throwables.propagate(e);
357         }
358     }
359
360     private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data) throws IOException {
361         final Iterator<DataContainerChild<? extends PathArgument, ?>> iterator = data.getValue().iterator();
362         while (iterator.hasNext()) {
363             final DataContainerChild<? extends PathArgument, ?> child = iterator.next();
364             nnWriter.write(child);
365             nnWriter.flush();
366         }
367     }
368
369     private static class DelegatingNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
370         private final NormalizedNodeStreamWriter delegate;
371
372         DelegatingNormalizedNodeStreamWriter(NormalizedNodeStreamWriter delegate) {
373             this.delegate = delegate;
374         }
375
376         @Override
377         public void leafNode(NodeIdentifier name, Object value) throws IOException, IllegalArgumentException {
378             delegate.leafNode(name, value);
379         }
380
381         @Override
382         public void startLeafSet(NodeIdentifier name, int childSizeHint) throws IOException, IllegalArgumentException {
383             delegate.startLeafSet(name, childSizeHint);
384         }
385
386         @Override
387         public void leafSetEntryNode(Object value) throws IOException, IllegalArgumentException {
388             delegate.leafSetEntryNode(value);
389         }
390
391         @Override
392         public void startContainerNode(NodeIdentifier name, int childSizeHint) throws IOException,
393                 IllegalArgumentException {
394             delegate.startContainerNode(name, childSizeHint);
395         }
396
397         @Override
398         public void startUnkeyedList(NodeIdentifier name, int childSizeHint) throws IOException,
399                 IllegalArgumentException {
400             delegate.startUnkeyedList(name, childSizeHint);
401         }
402
403         @Override
404         public void startUnkeyedListItem(NodeIdentifier name, int childSizeHint) throws IOException,
405                 IllegalStateException {
406             delegate.startUnkeyedListItem(name, childSizeHint);
407         }
408
409         @Override
410         public void startMapNode(NodeIdentifier name, int childSizeHint) throws IOException, IllegalArgumentException {
411             delegate.startMapNode(name, childSizeHint);
412         }
413
414         @Override
415         public void startMapEntryNode(NodeIdentifierWithPredicates identifier, int childSizeHint) throws IOException,
416                 IllegalArgumentException {
417             delegate.startMapEntryNode(identifier, childSizeHint);
418         }
419
420         @Override
421         public void startOrderedMapNode(NodeIdentifier name, int childSizeHint) throws IOException,
422                 IllegalArgumentException {
423             delegate.startOrderedMapNode(name, childSizeHint);
424         }
425
426         @Override
427         public void startChoiceNode(NodeIdentifier name, int childSizeHint) throws IOException,
428                 IllegalArgumentException {
429             delegate.startChoiceNode(name, childSizeHint);
430         }
431
432         @Override
433         public void startAugmentationNode(AugmentationIdentifier identifier) throws IOException,
434                 IllegalArgumentException {
435             delegate.startAugmentationNode(identifier);
436         }
437
438         @Override
439         public void anyxmlNode(NodeIdentifier name, Object value) throws IOException, IllegalArgumentException {
440             delegate.anyxmlNode(name, value);
441         }
442
443         @Override
444         public void startYangModeledAnyXmlNode(NodeIdentifier name, int childSizeHint) throws IOException {
445             delegate.startYangModeledAnyXmlNode(name, childSizeHint);
446         }
447
448         @Override
449         public void endNode() throws IOException, IllegalStateException {
450             delegate.endNode();
451         }
452
453         @Override
454         public void close() throws IOException {
455             delegate.close();
456         }
457
458         @Override
459         public void flush() throws IOException {
460             delegate.flush();
461         }
462     }
463 }