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