Merge "Bug 2358: Fixed warnings in Restconf"
[controller.git] / opendaylight / md-sal / sal-rest-connector / src / main / java / org / opendaylight / controller / 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.controller.sal.rest.impl;
10
11 import com.google.common.base.Charsets;
12 import com.google.common.base.Preconditions;
13 import com.google.common.base.Throwables;
14 import com.google.common.collect.Iterables;
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.OutputStreamWriter;
18 import java.net.URI;
19 import java.util.Iterator;
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.stream.FactoryConfigurationError;
28 import javax.xml.stream.XMLOutputFactory;
29 import javax.xml.stream.XMLStreamException;
30 import javax.xml.stream.XMLStreamWriter;
31 import org.opendaylight.controller.sal.rest.api.Draft02;
32 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
33 import org.opendaylight.controller.sal.restconf.impl.InstanceIdentifierContext;
34 import org.opendaylight.controller.sal.restconf.impl.NormalizedNodeContext;
35 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
36 import org.opendaylight.controller.sal.restconf.impl.RestconfError;
37 import org.opendaylight.yangtools.yang.common.QName;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
41 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
42 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
43 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
44 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
45 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
46 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
47 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
48 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
49 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter;
50 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
51 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
52 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder;
53 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder;
54 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
56 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
57 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
58 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
59 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
60 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 /**
65  * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
66  * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
67  *
68  * @author Thomas Pantelis
69  */
70 @Provider
71 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
72
73     private final static Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
74
75     private static final XMLOutputFactory XML_FACTORY;
76
77     static {
78         XML_FACTORY = XMLOutputFactory.newFactory();
79         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
80     }
81
82     @Context
83     private HttpHeaders headers;
84
85     @Override
86     public Response toResponse(final RestconfDocumentedException exception) {
87
88         LOG.debug("In toResponse: {}", exception.getMessage());
89
90         final List<MediaType> accepts = headers.getAcceptableMediaTypes();
91         accepts.remove(MediaType.WILDCARD_TYPE);
92
93         LOG.debug("Accept headers: {}", accepts);
94
95         final MediaType mediaType;
96         if (accepts != null && accepts.size() > 0) {
97             mediaType = accepts.get(0); // just pick the first one
98         } else {
99             // Default to the content type if there's no Accept header
100             mediaType = MediaType.APPLICATION_JSON_TYPE;
101         }
102
103         LOG.debug("Using MediaType: {}", mediaType);
104
105         final List<RestconfError> errors = exception.getErrors();
106         if (errors.isEmpty()) {
107             // We don't actually want to send any content but, if we don't set any content here,
108             // the tomcat front-end will send back an html error report. To prevent that, set a
109             // single space char in the entity.
110
111             return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
112         }
113
114         final int status = errors.iterator().next().getErrorTag().getStatusCode();
115
116         final ControllerContext context = ControllerContext.getInstance();
117         final DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode();
118
119         if (errorsSchemaNode == null) {
120             return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
121         }
122
123         Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
124                 "Found Errors SchemaNode isn't ContainerNode");
125         final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> errContBuild =
126                 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
127
128         final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
129                 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
130         final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
131         Preconditions.checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
132         final CollectionNodeBuilder<MapEntryNode, MapNode> listErorsBuilder = Builders
133                 .mapBuilder((ListSchemaNode) errListSchemaNode);
134
135
136         for (final RestconfError error : errors) {
137             listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
138         }
139         errContBuild.withChild(listErorsBuilder.build());
140
141         final NormalizedNodeContext errContext =  new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
142                 (DataSchemaNode) errorsSchemaNode, null, context.getGlobalSchema()), errContBuild.build());
143
144         Object responseBody;
145         if (mediaType.getSubtype().endsWith("json")) {
146             responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
147         } else {
148             responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
149         }
150
151         return Response.status(status).type(mediaType).entity(responseBody).build();
152     }
153
154     private MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
155         Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode,
156                 "errListSchemaNode has to be of type ListSchemaNode");
157         final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
158         final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = Builders
159                 .mapEntryBuilder(listStreamSchemaNode);
160
161         List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
162                 (listStreamSchemaNode), "error-type");
163         final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
164         Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode);
165         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
166                 .withValue(error.getErrorType().getErrorTypeTag()).build());
167
168         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
169                 (listStreamSchemaNode), "error-tag");
170         final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
171         Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode);
172         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
173                 .withValue(error.getErrorTag().getTagValue()).build());
174
175         if (error.getErrorAppTag() != null) {
176             lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
177                     (listStreamSchemaNode), "error-app-tag");
178             final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
179             Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
180             errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
181                     .withValue(error.getErrorAppTag()).build());
182         }
183
184         lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
185                 (listStreamSchemaNode), "error-message");
186         final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
187         Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode);
188         errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
189                 .withValue(error.getErrorMessage()).build());
190
191         // TODO : find how could we add possible "error-path" and "error-info"
192
193         return errNodeValues.build();
194     }
195
196     private Object toJsonResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) {
197
198         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
199         NormalizedNode<?, ?> data = errorsNode.getData();
200         final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
201         final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
202
203         SchemaPath path = context.getSchemaNode().getPath();
204         final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, Charsets.UTF_8);
205         if (data == null) {
206             throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
207         }
208
209         boolean isDataRoot = false;
210         URI initialNs = null;
211         if (SchemaPath.ROOT.equals(path)) {
212             isDataRoot = true;
213         } else {
214             path = path.getParent();
215             // FIXME: Add proper handling of reading root.
216         }
217         if(!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
218             initialNs = schema.getQName().getNamespace();
219         }
220         final NormalizedNodeStreamWriter jsonWriter = JSONNormalizedNodeStreamWriter.create(context.getSchemaContext(),path,initialNs,outputWriter);
221         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(jsonWriter);
222         try {
223             if(isDataRoot) {
224                 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
225             } else {
226                 if(data instanceof MapEntryNode) {
227                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild(((MapEntryNode) data)).build();
228                 }
229                 nnWriter.write(data);
230             }
231             nnWriter.flush();
232             outputWriter.flush();
233         }
234         catch (final IOException e) {
235             LOG.warn("Error writing error response body", e);
236         }
237
238         return outStream.toString();
239
240     }
241
242     private Object toXMLResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) {
243
244         final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
245         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
246
247         XMLStreamWriter xmlWriter;
248         try {
249             xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, "UTF-8");
250         } catch (final XMLStreamException e) {
251             throw new IllegalStateException(e);
252         } catch (final FactoryConfigurationError e) {
253             throw new IllegalStateException(e);
254         }
255         NormalizedNode<?, ?> data = errorsNode.getData();
256         SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
257
258         boolean isDataRoot = false;
259         if (SchemaPath.ROOT.equals(schemaPath)) {
260             isDataRoot = true;
261         } else {
262             schemaPath = schemaPath.getParent();
263         }
264
265         final NormalizedNodeStreamWriter streamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
266                 pathContext.getSchemaContext(), schemaPath);
267         final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
268         try {
269             if (isDataRoot) {
270                 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
271             } else {
272                 if (data instanceof MapEntryNode) {
273                     // Restconf allows returning one list item. We need to wrap it
274                     // in map node in order to serialize it properly
275                     data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
276                 }
277                 nnWriter.write(data);
278                 nnWriter.flush();
279             }
280         }
281         catch (final IOException e) {
282             LOG.warn("Error writing error response body.", e);
283         }
284
285         return outStream.toString();
286     }
287
288     private void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data)
289             throws IOException {
290         try {
291             final QName name = SchemaContext.NAME;
292             xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
293             for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
294                 nnWriter.write(child);
295             }
296             nnWriter.flush();
297             xmlWriter.writeEndElement();
298             xmlWriter.flush();
299         } catch (final XMLStreamException e) {
300             Throwables.propagate(e);
301         }
302     }
303
304     private void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data) throws IOException {
305         final Iterator<DataContainerChild<? extends PathArgument, ?>> iterator = data.getValue().iterator();
306         while(iterator.hasNext()) {
307             final DataContainerChild<? extends PathArgument, ?> child = iterator.next();
308             nnWriter.write(child);
309             nnWriter.flush();
310         }
311     }
312 }