Hide NormalizedNodeContext and WriterParameters
[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.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.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
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.core.Response.Status;
27 import javax.ws.rs.ext.ExceptionMapper;
28 import javax.ws.rs.ext.Provider;
29 import javax.xml.XMLConstants;
30 import javax.xml.stream.FactoryConfigurationError;
31 import javax.xml.stream.XMLOutputFactory;
32 import javax.xml.stream.XMLStreamException;
33 import javax.xml.stream.XMLStreamWriter;
34 import org.opendaylight.netconf.sal.rest.api.Draft02;
35 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
36 import org.opendaylight.restconf.common.ErrorTags;
37 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
38 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
39 import org.opendaylight.restconf.common.errors.RestconfError;
40 import org.opendaylight.yangtools.yang.common.QName;
41 import org.opendaylight.yangtools.yang.common.XMLNamespace;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
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.NormalizedNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
49 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
50 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
51 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
52 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
53 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
54 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
55 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
56 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
57 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
58 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
59 import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders;
60 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
62 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
63 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
65 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
66 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 /**
71  * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
72  * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
73  *
74  * @author Thomas Pantelis
75  */
76 @Provider
77 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
78
79     private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
80
81     private static final XMLOutputFactory XML_FACTORY;
82
83     static {
84         XML_FACTORY = XMLOutputFactory.newFactory();
85         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
86     }
87
88     @Context
89     private HttpHeaders headers;
90
91     private final ControllerContext controllerContext;
92
93     public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
94         this.controllerContext = requireNonNull(controllerContext);
95     }
96
97     @Override
98     public Response toResponse(final RestconfDocumentedException exception) {
99
100         LOG.debug("In toResponse: {}", exception.getMessage());
101
102         final List<MediaType> mediaTypeList = new ArrayList<>();
103         if (headers.getMediaType() != null) {
104             mediaTypeList.add(headers.getMediaType());
105         }
106
107         mediaTypeList.addAll(headers.getAcceptableMediaTypes());
108         final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
109                 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
110
111         LOG.debug("Using MediaType: {}", mediaType);
112
113         final List<RestconfError> errors = exception.getErrors();
114         if (errors.isEmpty()) {
115             // We don't actually want to send any content but, if we don't set any content here,
116             // the tomcat front-end will send back an html error report. To prevent that, set a
117             // single space char in the entity.
118
119             return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
120         }
121
122         final Status status = ErrorTags.statusOf(errors.iterator().next().getErrorTag());
123         final DataNodeContainer errorsSchemaNode =
124                 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
125         if (errorsSchemaNode == null) {
126             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
127         }
128
129         checkState(errorsSchemaNode instanceof ContainerSchemaNode, "Found Errors SchemaNode isn't ContainerNode");
130         final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
131                 SchemaAwareBuilders.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         checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
137         final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listErorsBuilder = SchemaAwareBuilders
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, controllerContext.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         checkArgument(errListSchemaNode instanceof ListSchemaNode,
161                 "errListSchemaNode has to be of type ListSchemaNode");
162         final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
163         final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = SchemaAwareBuilders
164                 .mapEntryBuilder(listStreamSchemaNode);
165
166         List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
167                 listStreamSchemaNode, "error-type");
168         final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
169         checkState(errTypSchemaNode instanceof LeafSchemaNode);
170         errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
171                 .withValue(error.getErrorType().elementBody()).build());
172
173         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
174                 listStreamSchemaNode, "error-tag");
175         final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
176         checkState(errTagSchemaNode instanceof LeafSchemaNode);
177         errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
178                 .withValue(error.getErrorTag().elementBody()).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             checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
185             errNodeValues.withChild(SchemaAwareBuilders.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         checkState(errMsgSchemaNode instanceof LeafSchemaNode);
193         errNodeValues.withChild(SchemaAwareBuilders.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,
210                                              final DataNodeContainer errorsSchemaNode) {
211         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
212         NormalizedNode data = errorsNode.getData();
213         final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
214         final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
215
216         SchemaPath path = context.getSchemaNode().getPath();
217         final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
218         if (data == null) {
219             throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
220         }
221
222         boolean isDataRoot = false;
223         XMLNamespace initialNs = null;
224         if (SchemaPath.ROOT.equals(path)) {
225             isDataRoot = true;
226         } else {
227             path = path.getParent();
228             // FIXME: Add proper handling of reading root.
229         }
230         if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
231             initialNs = schema.getQName().getNamespace();
232         }
233
234         final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
235         final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
236             JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), path,
237             initialNs, jsonWriter);
238
239         // We create a delegating writer to special-case error-info as error-info is defined as an empty
240         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
241         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
242         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
243         // for error-info.
244         final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
245             private boolean inOurLeaf;
246
247             @Override
248             protected NormalizedNodeStreamWriter delegate() {
249                 return jsonStreamWriter;
250             }
251
252             @Override
253             public void startLeafNode(final NodeIdentifier name) throws IOException {
254                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
255                     inOurLeaf = true;
256                     jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
257                 } else {
258                     super.startLeafNode(name);
259                 }
260             }
261
262             @Override
263             public void scalarValue(final Object value) throws IOException {
264                 if (inOurLeaf) {
265                     jsonWriter.value(value.toString());
266                 } else {
267                     super.scalarValue(value);
268                 }
269             }
270
271             @Override
272             public void endNode() throws IOException {
273                 if (inOurLeaf) {
274                     inOurLeaf = false;
275                 } else {
276                     super.endNode();
277                 }
278             }
279         };
280
281         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
282         try {
283             if (isDataRoot) {
284                 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
285             } else {
286                 if (data instanceof MapEntryNode) {
287                     data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
288                         .withChild((MapEntryNode) data)
289                         .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         return outStream.toString(StandardCharsets.UTF_8);
306     }
307
308     private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
309                                             final DataNodeContainer errorsSchemaNode) {
310         final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
311         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
312
313         final XMLStreamWriter xmlWriter;
314         try {
315             xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
316         } catch (final XMLStreamException | FactoryConfigurationError e) {
317             throw new IllegalStateException(e);
318         }
319         NormalizedNode data = errorsNode.getData();
320         SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
321
322         boolean isDataRoot = false;
323         if (SchemaPath.ROOT.equals(schemaPath)) {
324             isDataRoot = true;
325         } else {
326             schemaPath = schemaPath.getParent();
327         }
328
329         final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
330                 pathContext.getSchemaContext(), schemaPath);
331
332         // We create a delegating writer to special-case error-info as error-info is defined as an empty
333         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
334         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
335         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
336         // for error-info.
337         final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
338             private boolean inOurLeaf;
339
340             @Override
341             protected NormalizedNodeStreamWriter delegate() {
342                 return xmlStreamWriter;
343             }
344
345             @Override
346             public void startLeafNode(final NodeIdentifier name) throws IOException {
347                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
348                     String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
349                     try {
350                         xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
351                                 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
352                     } catch (XMLStreamException e) {
353                         throw new IOException("Error writing error-info", e);
354                     }
355                     inOurLeaf = true;
356                 } else {
357                     super.startLeafNode(name);
358                 }
359             }
360
361             @Override
362             public void scalarValue(final Object value) throws IOException {
363                 if (inOurLeaf) {
364                     try {
365                         xmlWriter.writeCharacters(value.toString());
366                     } catch (XMLStreamException e) {
367                         throw new IOException("Error writing error-info", e);
368                     }
369                 } else {
370                     super.scalarValue(value);
371                 }
372             }
373
374             @Override
375             public void endNode() throws IOException {
376                 if (inOurLeaf) {
377                     try {
378                         xmlWriter.writeEndElement();
379                     } catch (XMLStreamException e) {
380                         throw new IOException("Error writing error-info", e);
381                     }
382                     inOurLeaf = false;
383                 } else {
384                     super.endNode();
385                 }
386             }
387         };
388
389         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
390         try {
391             if (isDataRoot) {
392                 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
393             } else {
394                 if (data instanceof MapEntryNode) {
395                     // Restconf allows returning one list item. We need to wrap it
396                     // in map node in order to serialize it properly
397                     data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
398                         .addChild((MapEntryNode) data)
399                         .build();
400                 }
401                 nnWriter.write(data);
402                 nnWriter.flush();
403             }
404         } catch (final IOException e) {
405             LOG.warn("Error writing error response body.", e);
406         }
407
408         return outStream.toString(StandardCharsets.UTF_8);
409     }
410
411     private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
412                                          final ContainerNode data) throws IOException {
413         final QName name = SchemaContext.NAME;
414         try {
415             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
416             for (final DataContainerChild child : data.body()) {
417                 nnWriter.write(child);
418             }
419             nnWriter.flush();
420             xmlWriter.writeEndElement();
421             xmlWriter.flush();
422         } catch (final XMLStreamException e) {
423             throw new IOException("Failed to write elements", e);
424         }
425     }
426
427     private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
428                                       final ContainerNode data) throws IOException {
429         for (final DataContainerChild child : data.body()) {
430             nnWriter.write(child);
431             nnWriter.flush();
432         }
433     }
434 }