2 * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved.
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
9 package org.opendaylight.controller.sal.rest.impl;
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 com.google.gson.stream.JsonWriter;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.OutputStreamWriter;
20 import java.util.Iterator;
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.controller.sal.rest.api.Draft02;
34 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
35 import org.opendaylight.controller.sal.restconf.impl.InstanceIdentifierContext;
36 import org.opendaylight.controller.sal.restconf.impl.NormalizedNodeContext;
37 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
38 import org.opendaylight.controller.sal.restconf.impl.RestconfError;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
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.NormalizedNodeStreamWriter;
50 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
51 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
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.impl.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;
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.
73 * @author Thomas Pantelis
76 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
78 private final static Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
80 private static final XMLOutputFactory XML_FACTORY;
83 XML_FACTORY = XMLOutputFactory.newFactory();
84 XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
88 private HttpHeaders headers;
91 public Response toResponse(final RestconfDocumentedException exception) {
93 LOG.debug("In toResponse: {}", exception.getMessage());
95 final List<MediaType> accepts = headers.getAcceptableMediaTypes();
96 accepts.remove(MediaType.WILDCARD_TYPE);
98 LOG.debug("Accept headers: {}", accepts);
100 final MediaType mediaType;
101 if (accepts != null && accepts.size() > 0) {
102 mediaType = accepts.get(0); // just pick the first one
104 // Default to the content type if there's no Accept header
105 mediaType = MediaType.APPLICATION_JSON_TYPE;
108 LOG.debug("Using MediaType: {}", mediaType);
110 final List<RestconfError> errors = exception.getErrors();
111 if (errors.isEmpty()) {
112 // We don't actually want to send any content but, if we don't set any content here,
113 // the tomcat front-end will send back an html error report. To prevent that, set a
114 // single space char in the entity.
116 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
119 final int status = errors.iterator().next().getErrorTag().getStatusCode();
121 final ControllerContext context = ControllerContext.getInstance();
122 final DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode();
124 if (errorsSchemaNode == null) {
125 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
128 Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
129 "Found Errors SchemaNode isn't ContainerNode");
130 final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> errContBuild =
131 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
133 final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
134 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
135 final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
136 Preconditions.checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
137 final CollectionNodeBuilder<MapEntryNode, MapNode> listErorsBuilder = Builders
138 .mapBuilder((ListSchemaNode) errListSchemaNode);
141 for (final RestconfError error : errors) {
142 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
144 errContBuild.withChild(listErorsBuilder.build());
146 final NormalizedNodeContext errContext = new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
147 (DataSchemaNode) errorsSchemaNode, null, context.getGlobalSchema()), errContBuild.build());
150 if (mediaType.getSubtype().endsWith("json")) {
151 responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
153 responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
156 return Response.status(status).type(mediaType).entity(responseBody).build();
159 private MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
160 Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode,
161 "errListSchemaNode has to be of type ListSchemaNode");
162 final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
163 final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = Builders
164 .mapEntryBuilder(listStreamSchemaNode);
166 List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
167 (listStreamSchemaNode), "error-type");
168 final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
169 Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode);
170 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
171 .withValue(error.getErrorType().getErrorTypeTag()).build());
173 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
174 (listStreamSchemaNode), "error-tag");
175 final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
176 Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode);
177 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
178 .withValue(error.getErrorTag().getTagValue()).build());
180 if (error.getErrorAppTag() != null) {
181 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
182 (listStreamSchemaNode), "error-app-tag");
183 final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
184 Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
185 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
186 .withValue(error.getErrorAppTag()).build());
189 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
190 (listStreamSchemaNode), "error-message");
191 final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
192 Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode);
193 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
194 .withValue(error.getErrorMessage()).build());
196 if(error.getErrorInfo() != null) {
197 // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
198 // intention is for implementors to define their own data content so we'll just treat it as a leaf
200 errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
201 error.getErrorInfo()));
204 // TODO : find how could we add possible "error-path"
206 return errNodeValues.build();
209 private Object toJsonResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) {
211 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
212 NormalizedNode<?, ?> data = errorsNode.getData();
213 final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
214 final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
216 SchemaPath path = context.getSchemaNode().getPath();
217 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, Charsets.UTF_8);
219 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
222 boolean isDataRoot = false;
223 URI initialNs = null;
224 if (SchemaPath.ROOT.equals(path)) {
227 path = path.getParent();
228 // FIXME: Add proper handling of reading root.
230 if(!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
231 initialNs = schema.getQName().getNamespace();
234 final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
235 final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
236 JSONCodecFactory.create(context.getSchemaContext()), path, initialNs, jsonWriter);
238 // We create a delegating writer to special-case error-info as error-info is defined as an empty
239 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
240 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
241 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
243 final NormalizedNodeStreamWriter streamWriter = new DelegatingNormalizedNodeStreamWriter(jsonStreamWriter) {
245 public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
246 if(name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
247 jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
248 jsonWriter.value(value.toString());
250 super.leafNode(name, value);
255 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
258 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
260 if(data instanceof MapEntryNode) {
261 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild(((MapEntryNode) data)).build();
263 nnWriter.write(data);
266 outputWriter.flush();
268 catch (final IOException e) {
269 LOG.warn("Error writing error response body", e);
272 return outStream.toString();
276 private Object toXMLResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) {
278 final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
279 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
281 final XMLStreamWriter xmlWriter;
283 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, "UTF-8");
284 } catch (final XMLStreamException e) {
285 throw new IllegalStateException(e);
286 } catch (final FactoryConfigurationError e) {
287 throw new IllegalStateException(e);
289 NormalizedNode<?, ?> data = errorsNode.getData();
290 SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
292 boolean isDataRoot = false;
293 if (SchemaPath.ROOT.equals(schemaPath)) {
296 schemaPath = schemaPath.getParent();
299 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
300 pathContext.getSchemaContext(), schemaPath);
302 // We create a delegating writer to special-case error-info as error-info is defined as an empty
303 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
304 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
305 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
307 final NormalizedNodeStreamWriter streamWriter = new DelegatingNormalizedNodeStreamWriter(xmlStreamWriter) {
309 public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
310 if(name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
311 String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
313 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
314 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
315 xmlWriter.writeCharacters(value.toString());
316 xmlWriter.writeEndElement();
317 } catch (XMLStreamException e) {
318 throw new IOException("Error writing error-info", e);
321 super.leafNode(name, value);
326 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
329 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
331 if (data instanceof MapEntryNode) {
332 // Restconf allows returning one list item. We need to wrap it
333 // in map node in order to serialize it properly
334 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
336 nnWriter.write(data);
340 catch (final IOException e) {
341 LOG.warn("Error writing error response body.", e);
344 return outStream.toString();
347 private void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data)
350 final QName name = SchemaContext.NAME;
351 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
352 for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
353 nnWriter.write(child);
356 xmlWriter.writeEndElement();
358 } catch (final XMLStreamException e) {
359 Throwables.propagate(e);
363 private void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data) throws IOException {
364 final Iterator<DataContainerChild<? extends PathArgument, ?>> iterator = data.getValue().iterator();
365 while(iterator.hasNext()) {
366 final DataContainerChild<? extends PathArgument, ?> child = iterator.next();
367 nnWriter.write(child);
372 private static class DelegatingNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
373 private final NormalizedNodeStreamWriter delegate;
375 DelegatingNormalizedNodeStreamWriter(NormalizedNodeStreamWriter delegate) {
376 this.delegate = delegate;
380 public void leafNode(NodeIdentifier name, Object value) throws IOException, IllegalArgumentException {
381 delegate.leafNode(name, value);
385 public void startLeafSet(NodeIdentifier name, int childSizeHint) throws IOException, IllegalArgumentException {
386 delegate.startLeafSet(name, childSizeHint);
390 public void leafSetEntryNode(Object value) throws IOException, IllegalArgumentException {
391 delegate.leafSetEntryNode(value);
395 public void startContainerNode(NodeIdentifier name, int childSizeHint) throws IOException,
396 IllegalArgumentException {
397 delegate.startContainerNode(name, childSizeHint);
401 public void startUnkeyedList(NodeIdentifier name, int childSizeHint) throws IOException,
402 IllegalArgumentException {
403 delegate.startUnkeyedList(name, childSizeHint);
407 public void startUnkeyedListItem(NodeIdentifier name, int childSizeHint) throws IOException,
408 IllegalStateException {
409 delegate.startUnkeyedListItem(name, childSizeHint);
413 public void startMapNode(NodeIdentifier name, int childSizeHint) throws IOException, IllegalArgumentException {
414 delegate.startMapNode(name, childSizeHint);
418 public void startMapEntryNode(NodeIdentifierWithPredicates identifier, int childSizeHint) throws IOException,
419 IllegalArgumentException {
420 delegate.startMapEntryNode(identifier, childSizeHint);
424 public void startOrderedMapNode(NodeIdentifier name, int childSizeHint) throws IOException,
425 IllegalArgumentException {
426 delegate.startOrderedMapNode(name, childSizeHint);
430 public void startChoiceNode(NodeIdentifier name, int childSizeHint) throws IOException,
431 IllegalArgumentException {
432 delegate.startChoiceNode(name, childSizeHint);
436 public void startAugmentationNode(AugmentationIdentifier identifier) throws IOException,
437 IllegalArgumentException {
438 delegate.startAugmentationNode(identifier);
442 public void anyxmlNode(NodeIdentifier name, Object value) throws IOException, IllegalArgumentException {
443 delegate.anyxmlNode(name, value);
447 public void endNode() throws IOException, IllegalStateException {
452 public void close() throws IOException {
457 public void flush() throws IOException {