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() {
248 protected NormalizedNodeStreamWriter delegate() {
249 return jsonStreamWriter;
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());
258 super.leafNode(name, value);
263 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
266 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
268 if (data instanceof MapEntryNode) {
269 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild((MapEntryNode) data).build();
271 nnWriter.write(data);
274 outputWriter.flush();
275 } catch (final IOException e) {
276 LOG.warn("Error writing error response body", e);
280 return outStream.toString(StandardCharsets.UTF_8.name());
281 } catch (UnsupportedEncodingException e) {
283 return "Failure encoding error response: " + e;
287 private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
288 final DataNodeContainer errorsSchemaNode) {
289 final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
290 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
292 final XMLStreamWriter xmlWriter;
294 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
295 } catch (final XMLStreamException | FactoryConfigurationError e) {
296 throw new IllegalStateException(e);
298 NormalizedNode<?, ?> data = errorsNode.getData();
299 SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
301 boolean isDataRoot = false;
302 if (SchemaPath.ROOT.equals(schemaPath)) {
305 schemaPath = schemaPath.getParent();
308 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
309 pathContext.getSchemaContext(), schemaPath);
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
316 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
318 protected NormalizedNodeStreamWriter delegate() {
319 return xmlStreamWriter;
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();
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);
335 super.leafNode(name, value);
340 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
343 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
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();
350 nnWriter.write(data);
353 } catch (final IOException e) {
354 LOG.warn("Error writing error response body.", e);
358 return outStream.toString(StandardCharsets.UTF_8.name());
359 } catch (UnsupportedEncodingException e) {
361 return "Failure encoding error response: " + e;
365 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
366 final ContainerNode data) throws IOException {
367 final QName name = SchemaContext.NAME;
369 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
370 for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
371 nnWriter.write(child);
374 xmlWriter.writeEndElement();
376 } catch (final XMLStreamException e) {
377 throw new IOException("Failed to write elements", e);
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);