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