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