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
8 package org.opendaylight.netconf.sal.rest.impl;
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;
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;
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.
72 * @author Thomas Pantelis
75 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
77 private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
79 private static final XMLOutputFactory XML_FACTORY;
82 XML_FACTORY = XMLOutputFactory.newFactory();
83 XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
87 private HttpHeaders headers;
89 private final ControllerContext controllerContext;
91 public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
92 this.controllerContext = Preconditions.checkNotNull(controllerContext);
96 public Response toResponse(final RestconfDocumentedException exception) {
98 LOG.debug("In toResponse: {}", exception.getMessage());
100 final List<MediaType> mediaTypeList = new ArrayList<>();
101 if (headers.getMediaType() != null) {
102 mediaTypeList.add(headers.getMediaType());
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);
109 LOG.debug("Using MediaType: {}", mediaType);
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.
117 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
120 final int status = errors.iterator().next().getErrorTag().getStatusCode();
122 final DataNodeContainer errorsSchemaNode =
123 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
125 if (errorsSchemaNode == null) {
126 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
129 Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
130 "Found Errors SchemaNode isn't ContainerNode");
131 final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
132 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
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);
143 for (final RestconfError error : errors) {
144 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
146 errContBuild.withChild(listErorsBuilder.build());
148 final NormalizedNodeContext errContext = new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
149 (DataSchemaNode) errorsSchemaNode, null, controllerContext.getGlobalSchema()), errContBuild.build());
152 if (mediaType.getSubtype().endsWith("json")) {
153 responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
155 responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
158 return Response.status(status).type(mediaType).entity(responseBody).build();
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);
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());
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());
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());
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());
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
202 errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
203 error.getErrorInfo()));
206 // TODO : find how could we add possible "error-path"
208 return errNodeValues.build();
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();
218 SchemaPath path = context.getSchemaNode().getPath();
219 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
221 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
224 boolean isDataRoot = false;
225 URI initialNs = null;
226 if (SchemaPath.ROOT.equals(path)) {
229 path = path.getParent();
230 // FIXME: Add proper handling of reading root.
232 if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
233 initialNs = schema.getQName().getNamespace();
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);
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
246 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
247 private boolean inOurLeaf;
250 protected NormalizedNodeStreamWriter delegate() {
251 return jsonStreamWriter;
255 public void startLeafNode(final NodeIdentifier name) throws IOException {
256 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
258 jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
260 super.startLeafNode(name);
265 public void scalarValue(final Object value) throws IOException {
267 jsonWriter.value(value.toString());
269 super.scalarValue(value);
274 public void endNode() throws IOException {
283 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
286 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
288 if (data instanceof MapEntryNode) {
289 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild((MapEntryNode) data).build();
291 nnWriter.write(data);
294 outputWriter.flush();
295 } catch (final IOException e) {
296 LOG.warn("Error writing error response body", e);
300 streamWriter.close();
301 } catch (IOException e) {
302 LOG.warn("Failed to close stream writer", e);
306 return outStream.toString(StandardCharsets.UTF_8.name());
307 } catch (UnsupportedEncodingException e) {
309 return "Failure encoding error response: " + e;
313 private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
314 final DataNodeContainer errorsSchemaNode) {
315 final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
316 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
318 final XMLStreamWriter xmlWriter;
320 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
321 } catch (final XMLStreamException | FactoryConfigurationError e) {
322 throw new IllegalStateException(e);
324 NormalizedNode<?, ?> data = errorsNode.getData();
325 SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
327 boolean isDataRoot = false;
328 if (SchemaPath.ROOT.equals(schemaPath)) {
331 schemaPath = schemaPath.getParent();
334 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
335 pathContext.getSchemaContext(), schemaPath);
337 // We create a delegating writer to special-case error-info as error-info is defined as an empty
338 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
339 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
340 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
342 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
343 private boolean inOurLeaf;
346 protected NormalizedNodeStreamWriter delegate() {
347 return xmlStreamWriter;
351 public void startLeafNode(final NodeIdentifier name) throws IOException {
352 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
353 String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
355 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
356 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
357 } catch (XMLStreamException e) {
358 throw new IOException("Error writing error-info", e);
362 super.startLeafNode(name);
367 public void scalarValue(final Object value) throws IOException {
370 xmlWriter.writeCharacters(value.toString());
371 } catch (XMLStreamException e) {
372 throw new IOException("Error writing error-info", e);
375 super.scalarValue(value);
380 public void endNode() throws IOException {
383 xmlWriter.writeEndElement();
384 } catch (XMLStreamException e) {
385 throw new IOException("Error writing error-info", e);
394 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
397 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
399 if (data instanceof MapEntryNode) {
400 // Restconf allows returning one list item. We need to wrap it
401 // in map node in order to serialize it properly
402 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
404 nnWriter.write(data);
407 } catch (final IOException e) {
408 LOG.warn("Error writing error response body.", e);
412 return outStream.toString(StandardCharsets.UTF_8.name());
413 } catch (UnsupportedEncodingException e) {
415 return "Failure encoding error response: " + e;
419 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
420 final ContainerNode data) throws IOException {
421 final QName name = SchemaContext.NAME;
423 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
424 for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
425 nnWriter.write(child);
428 xmlWriter.writeEndElement();
430 } catch (final XMLStreamException e) {
431 throw new IOException("Failed to write elements", e);
435 private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
436 final ContainerNode data) throws IOException {
437 for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
438 nnWriter.write(child);