Enable modernizer in restconf-nb-bierman02
[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.net.URI;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
23 import javax.ws.rs.core.Context;
24 import javax.ws.rs.core.HttpHeaders;
25 import javax.ws.rs.core.MediaType;
26 import javax.ws.rs.core.Response;
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.context.InstanceIdentifierContext;
37 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
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.data.api.YangInstanceIdentifier.NodeIdentifier;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
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.MapNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
49 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
50 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
51 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
52 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
53 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
54 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
55 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
56 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
57 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
58 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder;
59 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeBuilder;
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 int status = errors.iterator().next().getErrorTag().getStatusCode();
123
124         final DataNodeContainer errorsSchemaNode =
125                 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
126
127         if (errorsSchemaNode == null) {
128             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
129         }
130
131         checkState(errorsSchemaNode instanceof ContainerSchemaNode, "Found Errors SchemaNode isn't ContainerNode");
132         final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
133                 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
134
135         final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
136                 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
137         final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
138         checkState(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         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         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         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             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         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         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.getNodeType()).addChild((MapEntryNode) data).build();
398                 }
399                 nnWriter.write(data);
400                 nnWriter.flush();
401             }
402         } catch (final IOException e) {
403             LOG.warn("Error writing error response body.", e);
404         }
405
406         return outStream.toString(StandardCharsets.UTF_8);
407     }
408
409     private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
410                                          final ContainerNode data) throws IOException {
411         final QName name = SchemaContext.NAME;
412         try {
413             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
414             for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
415                 nnWriter.write(child);
416             }
417             nnWriter.flush();
418             xmlWriter.writeEndElement();
419             xmlWriter.flush();
420         } catch (final XMLStreamException e) {
421             throw new IOException("Failed to write elements", e);
422         }
423     }
424
425     private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
426                                       final ContainerNode data) throws IOException {
427         for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
428             nnWriter.write(child);
429             nnWriter.flush();
430         }
431     }
432 }