Reduce the use of AttrBuilders
[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 com.google.common.base.Preconditions;
11 import com.google.common.collect.Iterables;
12 import com.google.gson.stream.JsonWriter;
13 import java.io.ByteArrayOutputStream;
14 import java.io.IOException;
15 import java.io.OutputStreamWriter;
16 import java.io.UnsupportedEncodingException;
17 import java.net.URI;
18 import java.nio.charset.StandardCharsets;
19 import java.util.ArrayList;
20 import java.util.List;
21 import javax.ws.rs.core.Context;
22 import javax.ws.rs.core.HttpHeaders;
23 import javax.ws.rs.core.MediaType;
24 import javax.ws.rs.core.Response;
25 import javax.ws.rs.ext.ExceptionMapper;
26 import javax.ws.rs.ext.Provider;
27 import javax.xml.XMLConstants;
28 import javax.xml.stream.FactoryConfigurationError;
29 import javax.xml.stream.XMLOutputFactory;
30 import javax.xml.stream.XMLStreamException;
31 import javax.xml.stream.XMLStreamWriter;
32 import org.opendaylight.netconf.sal.rest.api.Draft02;
33 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
34 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
35 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
36 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
37 import org.opendaylight.restconf.common.errors.RestconfError;
38 import org.opendaylight.yangtools.yang.common.QName;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
42 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
43 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
44 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
45 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
46 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
47 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
48 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
49 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
50 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
51 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
52 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
53 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
54 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
55 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
56 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder;
57 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeBuilder;
58 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
59 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
60 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
63 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
64 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 /**
69  * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
70  * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
71  *
72  * @author Thomas Pantelis
73  */
74 @Provider
75 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
76
77     private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
78
79     private static final XMLOutputFactory XML_FACTORY;
80
81     static {
82         XML_FACTORY = XMLOutputFactory.newFactory();
83         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
84     }
85
86     @Context
87     private HttpHeaders headers;
88
89     private final ControllerContext controllerContext;
90
91     public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
92         this.controllerContext = Preconditions.checkNotNull(controllerContext);
93     }
94
95     @Override
96     public Response toResponse(final RestconfDocumentedException exception) {
97
98         LOG.debug("In toResponse: {}", exception.getMessage());
99
100         final List<MediaType> mediaTypeList = new ArrayList<>();
101         if (headers.getMediaType() != null) {
102             mediaTypeList.add(headers.getMediaType());
103         }
104
105         mediaTypeList.addAll(headers.getAcceptableMediaTypes());
106         final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
107                 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
108
109         LOG.debug("Using MediaType: {}", mediaType);
110
111         final List<RestconfError> errors = exception.getErrors();
112         if (errors.isEmpty()) {
113             // We don't actually want to send any content but, if we don't set any content here,
114             // the tomcat front-end will send back an html error report. To prevent that, set a
115             // single space char in the entity.
116
117             return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
118         }
119
120         final int status = errors.iterator().next().getErrorTag().getStatusCode();
121
122         final DataNodeContainer errorsSchemaNode =
123                 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
124
125         if (errorsSchemaNode == null) {
126             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
127         }
128
129         Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
130                 "Found Errors SchemaNode isn't ContainerNode");
131         final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
132                 Builders.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         Preconditions.checkState(
138                 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         Preconditions.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         Preconditions.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         Preconditions.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             Preconditions.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         Preconditions.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             @Override
248             protected NormalizedNodeStreamWriter delegate() {
249                 return jsonStreamWriter;
250             }
251
252             @Override
253             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
254                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
255                     jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
256                     jsonWriter.value(value.toString());
257                 } else {
258                     super.leafNode(name, value);
259                 }
260             }
261         };
262
263         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
264         try {
265             if (isDataRoot) {
266                 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
267             } else {
268                 if (data instanceof MapEntryNode) {
269                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild((MapEntryNode) data).build();
270                 }
271                 nnWriter.write(data);
272             }
273             nnWriter.flush();
274             outputWriter.flush();
275         } catch (final IOException e) {
276             LOG.warn("Error writing error response body", e);
277         }
278
279         try {
280             return outStream.toString(StandardCharsets.UTF_8.name());
281         } catch (UnsupportedEncodingException e) {
282             // Shouldn't happen
283             return "Failure encoding error response: " + e;
284         }
285     }
286
287     private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
288                                             final DataNodeContainer errorsSchemaNode) {
289         final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
290         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
291
292         final XMLStreamWriter xmlWriter;
293         try {
294             xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
295         } catch (final XMLStreamException | FactoryConfigurationError e) {
296             throw new IllegalStateException(e);
297         }
298         NormalizedNode<?, ?> data = errorsNode.getData();
299         SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
300
301         boolean isDataRoot = false;
302         if (SchemaPath.ROOT.equals(schemaPath)) {
303             isDataRoot = true;
304         } else {
305             schemaPath = schemaPath.getParent();
306         }
307
308         final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
309                 pathContext.getSchemaContext(), schemaPath);
310
311         // We create a delegating writer to special-case error-info as error-info is defined as an empty
312         // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
313         // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
314         // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
315         // for error-info.
316         final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
317             @Override
318             protected NormalizedNodeStreamWriter delegate() {
319                 return xmlStreamWriter;
320             }
321
322             @Override
323             public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
324                 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
325                     String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
326                     try {
327                         xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
328                                 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
329                         xmlWriter.writeCharacters(value.toString());
330                         xmlWriter.writeEndElement();
331                     } catch (XMLStreamException e) {
332                         throw new IOException("Error writing error-info", e);
333                     }
334                 } else {
335                     super.leafNode(name, value);
336                 }
337             }
338         };
339
340         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
341         try {
342             if (isDataRoot) {
343                 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
344             } else {
345                 if (data instanceof MapEntryNode) {
346                     // Restconf allows returning one list item. We need to wrap it
347                     // in map node in order to serialize it properly
348                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
349                 }
350                 nnWriter.write(data);
351                 nnWriter.flush();
352             }
353         } catch (final IOException e) {
354             LOG.warn("Error writing error response body.", e);
355         }
356
357         try {
358             return outStream.toString(StandardCharsets.UTF_8.name());
359         } catch (UnsupportedEncodingException e) {
360             // Shouldn't happen
361             return "Failure encoding error response: " + e;
362         }
363     }
364
365     private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
366                                          final ContainerNode data) throws IOException {
367         final QName name = SchemaContext.NAME;
368         try {
369             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
370             for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
371                 nnWriter.write(child);
372             }
373             nnWriter.flush();
374             xmlWriter.writeEndElement();
375             xmlWriter.flush();
376         } catch (final XMLStreamException e) {
377             throw new IOException("Failed to write elements", e);
378         }
379     }
380
381     private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
382                                       final ContainerNode data) throws IOException {
383         for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
384             nnWriter.write(child);
385             nnWriter.flush();
386         }
387     }
388 }