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.netconf.sal.rest.impl;
11 import com.google.common.base.Preconditions;
12 import com.google.common.collect.Iterables;
13 import com.google.gson.stream.JsonWriter;
14 import java.io.ByteArrayOutputStream;
15 import java.io.IOException;
16 import java.io.OutputStreamWriter;
17 import java.io.UnsupportedEncodingException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
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.netconf.sal.rest.api.Draft02;
34 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
35 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
36 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
37 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
38 import org.opendaylight.restconf.common.errors.RestconfError;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
43 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
44 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
45 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
46 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
47 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
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.JSONCodecFactorySupplier;
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.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 static final 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;
90 private final ControllerContext controllerContext;
92 public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
93 this.controllerContext = Preconditions.checkNotNull(controllerContext);
97 public Response toResponse(final RestconfDocumentedException exception) {
99 LOG.debug("In toResponse: {}", exception.getMessage());
101 final List<MediaType> mediaTypeList = new ArrayList<>();
102 if (headers.getMediaType() != null) {
103 mediaTypeList.add(headers.getMediaType());
106 mediaTypeList.addAll(headers.getAcceptableMediaTypes());
107 final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
108 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
110 LOG.debug("Using MediaType: {}", mediaType);
112 final List<RestconfError> errors = exception.getErrors();
113 if (errors.isEmpty()) {
114 // We don't actually want to send any content but, if we don't set any content here,
115 // the tomcat front-end will send back an html error report. To prevent that, set a
116 // single space char in the entity.
118 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
121 final int status = errors.iterator().next().getErrorTag().getStatusCode();
123 final DataNodeContainer errorsSchemaNode =
124 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
126 if (errorsSchemaNode == null) {
127 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
130 Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode,
131 "Found Errors SchemaNode isn't ContainerNode");
132 final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> errContBuild =
133 Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
135 final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
136 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
137 final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
138 Preconditions.checkState(
139 errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
140 final CollectionNodeBuilder<MapEntryNode, MapNode> listErorsBuilder = Builders
141 .mapBuilder((ListSchemaNode) errListSchemaNode);
144 for (final RestconfError error : errors) {
145 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
147 errContBuild.withChild(listErorsBuilder.build());
149 final NormalizedNodeContext errContext = new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
150 (DataSchemaNode) errorsSchemaNode, null, controllerContext.getGlobalSchema()), errContBuild.build());
153 if (mediaType.getSubtype().endsWith("json")) {
154 responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
156 responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
159 return Response.status(status).type(mediaType).entity(responseBody).build();
162 private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
163 Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode,
164 "errListSchemaNode has to be of type ListSchemaNode");
165 final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
166 final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = Builders
167 .mapEntryBuilder(listStreamSchemaNode);
169 List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
170 listStreamSchemaNode, "error-type");
171 final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
172 Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode);
173 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
174 .withValue(error.getErrorType().getErrorTypeTag()).build());
176 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
177 listStreamSchemaNode, "error-tag");
178 final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
179 Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode);
180 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
181 .withValue(error.getErrorTag().getTagValue()).build());
183 if (error.getErrorAppTag() != null) {
184 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
185 listStreamSchemaNode, "error-app-tag");
186 final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
187 Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
188 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
189 .withValue(error.getErrorAppTag()).build());
192 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
193 listStreamSchemaNode, "error-message");
194 final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
195 Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode);
196 errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
197 .withValue(error.getErrorMessage()).build());
199 if (error.getErrorInfo() != null) {
200 // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
201 // intention is for implementors to define their own data content so we'll just treat it as a leaf
203 errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
204 error.getErrorInfo()));
207 // TODO : find how could we add possible "error-path"
209 return errNodeValues.build();
212 private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode,
213 final DataNodeContainer errorsSchemaNode) {
214 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
215 NormalizedNode<?, ?> data = errorsNode.getData();
216 final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
217 final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
219 SchemaPath path = context.getSchemaNode().getPath();
220 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
222 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
225 boolean isDataRoot = false;
226 URI initialNs = null;
227 if (SchemaPath.ROOT.equals(path)) {
230 path = path.getParent();
231 // FIXME: Add proper handling of reading root.
233 if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
234 initialNs = schema.getQName().getNamespace();
237 final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
238 final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
239 JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), path,
240 initialNs, jsonWriter);
242 // We create a delegating writer to special-case error-info as error-info is defined as an empty
243 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
244 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
245 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
247 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
249 protected NormalizedNodeStreamWriter delegate() {
250 return jsonStreamWriter;
254 public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
255 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
256 jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
257 jsonWriter.value(value.toString());
259 super.leafNode(name, value);
264 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
267 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
269 if (data instanceof MapEntryNode) {
270 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild((MapEntryNode) data).build();
272 nnWriter.write(data);
275 outputWriter.flush();
276 } catch (final IOException e) {
277 LOG.warn("Error writing error response body", e);
281 return outStream.toString(StandardCharsets.UTF_8.name());
282 } catch (UnsupportedEncodingException e) {
284 return "Failure encoding error response: " + e;
288 private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
289 final DataNodeContainer errorsSchemaNode) {
290 final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
291 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
293 final XMLStreamWriter xmlWriter;
295 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
296 } catch (final XMLStreamException | FactoryConfigurationError e) {
297 throw new IllegalStateException(e);
299 NormalizedNode<?, ?> data = errorsNode.getData();
300 SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
302 boolean isDataRoot = false;
303 if (SchemaPath.ROOT.equals(schemaPath)) {
306 schemaPath = schemaPath.getParent();
309 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
310 pathContext.getSchemaContext(), schemaPath);
312 // We create a delegating writer to special-case error-info as error-info is defined as an empty
313 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
314 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
315 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
317 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
319 protected NormalizedNodeStreamWriter delegate() {
320 return xmlStreamWriter;
324 public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
325 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
326 String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
328 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
329 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
330 xmlWriter.writeCharacters(value.toString());
331 xmlWriter.writeEndElement();
332 } catch (XMLStreamException e) {
333 throw new IOException("Error writing error-info", e);
336 super.leafNode(name, value);
341 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
344 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
346 if (data instanceof MapEntryNode) {
347 // Restconf allows returning one list item. We need to wrap it
348 // in map node in order to serialize it properly
349 data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build();
351 nnWriter.write(data);
354 } catch (final IOException e) {
355 LOG.warn("Error writing error response body.", e);
359 return outStream.toString(StandardCharsets.UTF_8.name());
360 } catch (UnsupportedEncodingException e) {
362 return "Failure encoding error response: " + e;
366 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
367 final ContainerNode data) throws IOException {
368 final QName name = SchemaContext.NAME;
370 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
371 for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
372 nnWriter.write(child);
375 xmlWriter.writeEndElement();
377 } catch (final XMLStreamException e) {
378 throw new IOException("Failed to write elements", e);
382 private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
383 final ContainerNode data) throws IOException {
384 for (final DataContainerChild<? extends PathArgument, ?> child : data.getValue()) {
385 nnWriter.write(child);