430f36641229e4e6bf70abfba2627a6e6dcbd858
[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.base.Throwables;
13 import com.google.common.collect.Iterables;
14 import com.google.gson.stream.JsonWriter;
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.OutputStreamWriter;
18 import java.io.UnsupportedEncodingException;
19 import java.net.URI;
20 import java.nio.charset.StandardCharsets;
21 import java.util.Iterator;
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.JSONCodecFactory;
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.DataContainerNodeAttrBuilder;
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     @Override
92     public Response toResponse(final RestconfDocumentedException exception) {
93
94         LOG.debug("In toResponse: {}", exception.getMessage());
95
96         final List<MediaType> accepts = headers.getAcceptableMediaTypes();
97         if (accepts != null) {
98             accepts.remove(MediaType.WILDCARD_TYPE);
99         }
100
101         LOG.debug("Accept headers: {}", accepts);
102
103         final MediaType mediaType;
104         if (accepts != null && accepts.size() > 0) {
105             mediaType = accepts.get(0); // just pick the first one
106         } else {
107             // Default to the content type if there's no Accept header
108             mediaType = MediaType.APPLICATION_JSON_TYPE;
109         }
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 ControllerContext context = ControllerContext.getInstance();
125         final DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode();
126
127         if (errorsSchemaNode == null) {
128             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
129         }
130
131         Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
132                 "Found Errors SchemaNode isn't ContainerNode");
133         final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> errContBuild =
134                 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
135
136         final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
137                 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
138         final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
139         Preconditions.checkState(
140                 errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
141         final CollectionNodeBuilder<MapEntryNode, MapNode> listErorsBuilder = Builders
142                 .mapBuilder((ListSchemaNode) errListSchemaNode);
143
144
145         for (final RestconfError error : errors) {
146             listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
147         }
148         errContBuild.withChild(listErorsBuilder.build());
149
150         final NormalizedNodeContext errContext =  new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
151                 (DataSchemaNode) errorsSchemaNode, null, context.getGlobalSchema()), errContBuild.build());
152
153         Object responseBody;
154         if (mediaType.getSubtype().endsWith("json")) {
155             responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
156         } else {
157             responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
158         }
159
160         return Response.status(status).type(mediaType).entity(responseBody).build();
161     }
162
163     private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
164         Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode,
165                 "errListSchemaNode has to be of type ListSchemaNode");
166         final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
167         final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = Builders
168                 .mapEntryBuilder(listStreamSchemaNode);
169
170         List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
171                 listStreamSchemaNode, "error-type");
172         final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
173         Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode);
174         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
175                 .withValue(error.getErrorType().getErrorTypeTag()).build());
176
177         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
178                 listStreamSchemaNode, "error-tag");
179         final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
180         Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode);
181         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
182                 .withValue(error.getErrorTag().getTagValue()).build());
183
184         if (error.getErrorAppTag() != null) {
185             lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
186                     listStreamSchemaNode, "error-app-tag");
187             final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
188             Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
189             errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
190                     .withValue(error.getErrorAppTag()).build());
191         }
192
193         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
194                 listStreamSchemaNode, "error-message");
195         final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
196         Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode);
197         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
198                 .withValue(error.getErrorMessage()).build());
199
200         if (error.getErrorInfo() != null) {
201             // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
202             // intention is for implementors to define their own data content so we'll just treat it as a leaf
203             // with string data.
204             errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
205                     error.getErrorInfo()));
206         }
207
208         // TODO : find how could we add possible "error-path"
209
210         return errNodeValues.build();
211     }
212
213     private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode,
214                                              final DataNodeContainer errorsSchemaNode) {
215         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
216         NormalizedNode<?, ?> data = errorsNode.getData();
217         final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
218         final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
219
220         SchemaPath path = context.getSchemaNode().getPath();
221         final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
222         if (data == null) {
223             throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
224         }
225
226         boolean isDataRoot = false;
227         URI initialNs = null;
228         if (SchemaPath.ROOT.equals(path)) {
229             isDataRoot = true;
230         } else {
231             path = path.getParent();
232             // FIXME: Add proper handling of reading root.
233         }
234         if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
235             initialNs = schema.getQName().getNamespace();
236         }
237
238         final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
239         final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
240                 JSONCodecFactory.getShared(context.getSchemaContext()), path, 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 e) {
297             throw new IllegalStateException(e);
298         } catch (final FactoryConfigurationError e) {
299             throw new IllegalStateException(e);
300         }
301         NormalizedNode<?, ?> data = errorsNode.getData();
302         SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
303
304         boolean isDataRoot = false;
305         if (SchemaPath.ROOT.equals(schemaPath)) {
306             isDataRoot = true;
307         } else {
308             schemaPath = schemaPath.getParent();
309         }
310
311         final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
312                 pathContext.getSchemaContext(), schemaPath);
313
314         // We create a delegating writer to special-case error-info as error-info is defined as an empty
315         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
316         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
317         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
318         // for error-info.
319         final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
320             @Override
321             protected NormalizedNodeStreamWriter delegate() {
322                 return xmlStreamWriter;
323             }
324
325             @Override
326             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
327                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
328                     String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
329                     try {
330                         xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
331                                 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
332                         xmlWriter.writeCharacters(value.toString());
333                         xmlWriter.writeEndElement();
334                     } catch (XMLStreamException e) {
335                         throw new IOException("Error writing error-info", e);
336                     }
337                 } else {
338                     super.leafNode(name, value);
339                 }
340             }
341         };
342
343         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
344         try {
345             if (isDataRoot) {
346                 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
347             } else {
348                 if (data instanceof MapEntryNode) {
349                     // Restconf allows returning one list item. We need to wrap it
350                     // in map node in order to serialize it properly
351                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
352                 }
353                 nnWriter.write(data);
354                 nnWriter.flush();
355             }
356         } catch (final IOException e) {
357             LOG.warn("Error writing error response body.", e);
358         }
359
360         try {
361             return outStream.toString(StandardCharsets.UTF_8.name());
362         } catch (UnsupportedEncodingException e) {
363             // Shouldn't happen
364             return "Failure encoding error response: " + e;
365         }
366     }
367
368     private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
369                                          final ContainerNode data)
370             throws IOException {
371         try {
372             final QName name = SchemaContext.NAME;
373             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
374             for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
375                 nnWriter.write(child);
376             }
377             nnWriter.flush();
378             xmlWriter.writeEndElement();
379             xmlWriter.flush();
380         } catch (final XMLStreamException e) {
381             Throwables.propagate(e);
382         }
383     }
384
385     private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
386                                       final ContainerNode data) throws IOException {
387         final Iterator<DataContainerChild<? extends PathArgument, ?>> iterator = data.getValue().iterator();
388         while (iterator.hasNext()) {
389             final DataContainerChild<? extends PathArgument, ?> child = iterator.next();
390             nnWriter.write(child);
391             nnWriter.flush();
392         }
393     }
394 }