a858efbd292addf61f74412c9521f62e2fd3f237
[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
9 package org.opendaylight.netconf.sal.rest.impl;
10
11 import com.google.common.base.Preconditions;
12 import com.google.common.collect.Iterables;
13 import com.google.gson.stream.JsonWriter;
14 import java.io.ByteArrayOutputStream;
15 import java.io.IOException;
16 import java.io.OutputStreamWriter;
17 import java.io.UnsupportedEncodingException;
18 import java.net.URI;
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.data.api.YangInstanceIdentifier.NodeIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
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.MapNode;
47 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
49 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
50 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
51 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
52 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
53 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
54 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
55 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
56 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
57 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder;
58 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder;
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 = Preconditions.checkNotNull(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         Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
131                 "Found Errors SchemaNode isn't ContainerNode");
132         final DataContainerNodeAttrBuilder<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         Preconditions.checkState(
139                 errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
140         final CollectionNodeBuilder<MapEntryNode, MapNode> listErorsBuilder = Builders
141                 .mapBuilder((ListSchemaNode) errListSchemaNode);
142
143
144         for (final RestconfError error : errors) {
145             listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
146         }
147         errContBuild.withChild(listErorsBuilder.build());
148
149         final NormalizedNodeContext errContext =  new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
150                 (DataSchemaNode) errorsSchemaNode, null, controllerContext.getGlobalSchema()), errContBuild.build());
151
152         Object responseBody;
153         if (mediaType.getSubtype().endsWith("json")) {
154             responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
155         } else {
156             responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
157         }
158
159         return Response.status(status).type(mediaType).entity(responseBody).build();
160     }
161
162     private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
163         Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode,
164                 "errListSchemaNode has to be of type ListSchemaNode");
165         final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
166         final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = Builders
167                 .mapEntryBuilder(listStreamSchemaNode);
168
169         List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
170                 listStreamSchemaNode, "error-type");
171         final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
172         Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode);
173         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
174                 .withValue(error.getErrorType().getErrorTypeTag()).build());
175
176         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
177                 listStreamSchemaNode, "error-tag");
178         final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
179         Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode);
180         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
181                 .withValue(error.getErrorTag().getTagValue()).build());
182
183         if (error.getErrorAppTag() != null) {
184             lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
185                     listStreamSchemaNode, "error-app-tag");
186             final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
187             Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
188             errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
189                     .withValue(error.getErrorAppTag()).build());
190         }
191
192         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
193                 listStreamSchemaNode, "error-message");
194         final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
195         Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode);
196         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
197                 .withValue(error.getErrorMessage()).build());
198
199         if (error.getErrorInfo() != null) {
200             // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
201             // intention is for implementors to define their own data content so we'll just treat it as a leaf
202             // with string data.
203             errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
204                     error.getErrorInfo()));
205         }
206
207         // TODO : find how could we add possible "error-path"
208
209         return errNodeValues.build();
210     }
211
212     private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode,
213                                              final DataNodeContainer errorsSchemaNode) {
214         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
215         NormalizedNode<?, ?> data = errorsNode.getData();
216         final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
217         final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
218
219         SchemaPath path = context.getSchemaNode().getPath();
220         final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
221         if (data == null) {
222             throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
223         }
224
225         boolean isDataRoot = false;
226         URI initialNs = null;
227         if (SchemaPath.ROOT.equals(path)) {
228             isDataRoot = true;
229         } else {
230             path = path.getParent();
231             // FIXME: Add proper handling of reading root.
232         }
233         if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
234             initialNs = schema.getQName().getNamespace();
235         }
236
237         final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
238         final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
239             JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), path,
240             initialNs, jsonWriter);
241
242         // We create a delegating writer to special-case error-info as error-info is defined as an empty
243         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
244         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
245         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
246         // for error-info.
247         final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
248             @Override
249             protected NormalizedNodeStreamWriter delegate() {
250                 return jsonStreamWriter;
251             }
252
253             @Override
254             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
255                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
256                     jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
257                     jsonWriter.value(value.toString());
258                 } else {
259                     super.leafNode(name, value);
260                 }
261             }
262         };
263
264         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
265         try {
266             if (isDataRoot) {
267                 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
268             } else {
269                 if (data instanceof MapEntryNode) {
270                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild((MapEntryNode) data).build();
271                 }
272                 nnWriter.write(data);
273             }
274             nnWriter.flush();
275             outputWriter.flush();
276         } catch (final IOException e) {
277             LOG.warn("Error writing error response body", e);
278         }
279
280         try {
281             return outStream.toString(StandardCharsets.UTF_8.name());
282         } catch (UnsupportedEncodingException e) {
283             // Shouldn't happen
284             return "Failure encoding error response: " + e;
285         }
286     }
287
288     private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
289                                             final DataNodeContainer errorsSchemaNode) {
290         final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
291         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
292
293         final XMLStreamWriter xmlWriter;
294         try {
295             xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
296         } catch (final XMLStreamException | FactoryConfigurationError e) {
297             throw new IllegalStateException(e);
298         }
299         NormalizedNode<?, ?> data = errorsNode.getData();
300         SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
301
302         boolean isDataRoot = false;
303         if (SchemaPath.ROOT.equals(schemaPath)) {
304             isDataRoot = true;
305         } else {
306             schemaPath = schemaPath.getParent();
307         }
308
309         final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
310                 pathContext.getSchemaContext(), schemaPath);
311
312         // We create a delegating writer to special-case error-info as error-info is defined as an empty
313         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
314         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
315         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
316         // for error-info.
317         final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
318             @Override
319             protected NormalizedNodeStreamWriter delegate() {
320                 return xmlStreamWriter;
321             }
322
323             @Override
324             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
325                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
326                     String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
327                     try {
328                         xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
329                                 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
330                         xmlWriter.writeCharacters(value.toString());
331                         xmlWriter.writeEndElement();
332                     } catch (XMLStreamException e) {
333                         throw new IOException("Error writing error-info", e);
334                     }
335                 } else {
336                     super.leafNode(name, value);
337                 }
338             }
339         };
340
341         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
342         try {
343             if (isDataRoot) {
344                 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
345             } else {
346                 if (data instanceof MapEntryNode) {
347                     // Restconf allows returning one list item. We need to wrap it
348                     // in map node in order to serialize it properly
349                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
350                 }
351                 nnWriter.write(data);
352                 nnWriter.flush();
353             }
354         } catch (final IOException e) {
355             LOG.warn("Error writing error response body.", e);
356         }
357
358         try {
359             return outStream.toString(StandardCharsets.UTF_8.name());
360         } catch (UnsupportedEncodingException e) {
361             // Shouldn't happen
362             return "Failure encoding error response: " + e;
363         }
364     }
365
366     private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
367                                          final ContainerNode data) throws IOException {
368         final QName name = SchemaContext.NAME;
369         try {
370             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
371             for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
372                 nnWriter.write(child);
373             }
374             nnWriter.flush();
375             xmlWriter.writeEndElement();
376             xmlWriter.flush();
377         } catch (final XMLStreamException e) {
378             throw new IOException("Failed to write elements", e);
379         }
380     }
381
382     private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
383                                       final ContainerNode data) throws IOException {
384         for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
385             nnWriter.write(child);
386             nnWriter.flush();
387         }
388     }
389 }