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