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