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 static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static java.util.Objects.requireNonNull;
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;
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.core.Response.Status;
27 import javax.ws.rs.ext.ExceptionMapper;
28 import javax.ws.rs.ext.Provider;
29 import javax.xml.XMLConstants;
30 import javax.xml.stream.FactoryConfigurationError;
31 import javax.xml.stream.XMLOutputFactory;
32 import javax.xml.stream.XMLStreamException;
33 import javax.xml.stream.XMLStreamWriter;
34 import org.opendaylight.netconf.sal.rest.api.Draft02;
35 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
36 import org.opendaylight.restconf.common.ErrorTags;
37 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
38 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
39 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
40 import org.opendaylight.restconf.common.errors.RestconfError;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.common.XMLNamespace;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
44 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
45 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
46 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
47 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
49 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
50 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
51 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
52 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
53 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
54 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
55 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
56 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
57 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
58 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
59 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
60 import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders;
61 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
63 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
65 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
66 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
67 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
72 * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
73 * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
75 * @author Thomas Pantelis
78 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
80 private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
82 private static final XMLOutputFactory XML_FACTORY;
85 XML_FACTORY = XMLOutputFactory.newFactory();
86 XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
90 private HttpHeaders headers;
92 private final ControllerContext controllerContext;
94 public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
95 this.controllerContext = requireNonNull(controllerContext);
99 public Response toResponse(final RestconfDocumentedException exception) {
101 LOG.debug("In toResponse: {}", exception.getMessage());
103 final List<MediaType> mediaTypeList = new ArrayList<>();
104 if (headers.getMediaType() != null) {
105 mediaTypeList.add(headers.getMediaType());
108 mediaTypeList.addAll(headers.getAcceptableMediaTypes());
109 final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
110 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
112 LOG.debug("Using MediaType: {}", mediaType);
114 final List<RestconfError> errors = exception.getErrors();
115 if (errors.isEmpty()) {
116 // We don't actually want to send any content but, if we don't set any content here,
117 // the tomcat front-end will send back an html error report. To prevent that, set a
118 // single space char in the entity.
120 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
123 final Status status = ErrorTags.statusOf(errors.iterator().next().getErrorTag());
124 final DataNodeContainer errorsSchemaNode =
125 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
126 if (errorsSchemaNode == null) {
127 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
130 checkState(errorsSchemaNode instanceof ContainerSchemaNode, "Found Errors SchemaNode isn't ContainerNode");
131 final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
132 SchemaAwareBuilders.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 checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
138 final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listErorsBuilder = SchemaAwareBuilders
139 .mapBuilder((ListSchemaNode) errListSchemaNode);
142 for (final RestconfError error : errors) {
143 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
145 errContBuild.withChild(listErorsBuilder.build());
147 final NormalizedNodeContext errContext = new NormalizedNodeContext(new InstanceIdentifierContext<>(null,
148 (DataSchemaNode) errorsSchemaNode, null, controllerContext.getGlobalSchema()), errContBuild.build());
151 if (mediaType.getSubtype().endsWith("json")) {
152 responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
154 responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
157 return Response.status(status).type(mediaType).entity(responseBody).build();
160 private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
161 checkArgument(errListSchemaNode instanceof ListSchemaNode,
162 "errListSchemaNode has to be of type ListSchemaNode");
163 final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
164 final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = SchemaAwareBuilders
165 .mapEntryBuilder(listStreamSchemaNode);
167 List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
168 listStreamSchemaNode, "error-type");
169 final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
170 checkState(errTypSchemaNode instanceof LeafSchemaNode);
171 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
172 .withValue(error.getErrorType().elementBody()).build());
174 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
175 listStreamSchemaNode, "error-tag");
176 final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
177 checkState(errTagSchemaNode instanceof LeafSchemaNode);
178 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
179 .withValue(error.getErrorTag().elementBody()).build());
181 if (error.getErrorAppTag() != null) {
182 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
183 listStreamSchemaNode, "error-app-tag");
184 final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
185 checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
186 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
187 .withValue(error.getErrorAppTag()).build());
190 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
191 listStreamSchemaNode, "error-message");
192 final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
193 checkState(errMsgSchemaNode instanceof LeafSchemaNode);
194 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
195 .withValue(error.getErrorMessage()).build());
197 if (error.getErrorInfo() != null) {
198 // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
199 // intention is for implementors to define their own data content so we'll just treat it as a leaf
201 errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
202 error.getErrorInfo()));
205 // TODO : find how could we add possible "error-path"
207 return errNodeValues.build();
210 private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode,
211 final DataNodeContainer errorsSchemaNode) {
212 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
213 NormalizedNode data = errorsNode.getData();
214 final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
215 final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
217 SchemaPath path = context.getSchemaNode().getPath();
218 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
220 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
223 boolean isDataRoot = false;
224 XMLNamespace initialNs = null;
225 if (SchemaPath.ROOT.equals(path)) {
228 path = path.getParent();
229 // FIXME: Add proper handling of reading root.
231 if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
232 initialNs = schema.getQName().getNamespace();
235 final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
236 final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
237 JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), path,
238 initialNs, jsonWriter);
240 // We create a delegating writer to special-case error-info as error-info is defined as an empty
241 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
242 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
243 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
245 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
246 private boolean inOurLeaf;
249 protected NormalizedNodeStreamWriter delegate() {
250 return jsonStreamWriter;
254 public void startLeafNode(final NodeIdentifier name) throws IOException {
255 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
257 jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
259 super.startLeafNode(name);
264 public void scalarValue(final Object value) throws IOException {
266 jsonWriter.value(value.toString());
268 super.scalarValue(value);
273 public void endNode() throws IOException {
282 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
285 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
287 if (data instanceof MapEntryNode) {
288 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
289 .withChild((MapEntryNode) data)
292 nnWriter.write(data);
295 outputWriter.flush();
296 } catch (final IOException e) {
297 LOG.warn("Error writing error response body", e);
301 streamWriter.close();
302 } catch (IOException e) {
303 LOG.warn("Failed to close stream writer", e);
306 return outStream.toString(StandardCharsets.UTF_8);
309 private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
310 final DataNodeContainer errorsSchemaNode) {
311 final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
312 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
314 final XMLStreamWriter xmlWriter;
316 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
317 } catch (final XMLStreamException | FactoryConfigurationError e) {
318 throw new IllegalStateException(e);
320 NormalizedNode data = errorsNode.getData();
321 SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
323 boolean isDataRoot = false;
324 if (SchemaPath.ROOT.equals(schemaPath)) {
327 schemaPath = schemaPath.getParent();
330 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
331 pathContext.getSchemaContext(), schemaPath);
333 // We create a delegating writer to special-case error-info as error-info is defined as an empty
334 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
335 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
336 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
338 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
339 private boolean inOurLeaf;
342 protected NormalizedNodeStreamWriter delegate() {
343 return xmlStreamWriter;
347 public void startLeafNode(final NodeIdentifier name) throws IOException {
348 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
349 String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
351 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
352 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
353 } catch (XMLStreamException e) {
354 throw new IOException("Error writing error-info", e);
358 super.startLeafNode(name);
363 public void scalarValue(final Object value) throws IOException {
366 xmlWriter.writeCharacters(value.toString());
367 } catch (XMLStreamException e) {
368 throw new IOException("Error writing error-info", e);
371 super.scalarValue(value);
376 public void endNode() throws IOException {
379 xmlWriter.writeEndElement();
380 } catch (XMLStreamException e) {
381 throw new IOException("Error writing error-info", e);
390 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
393 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
395 if (data instanceof MapEntryNode) {
396 // Restconf allows returning one list item. We need to wrap it
397 // in map node in order to serialize it properly
398 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
399 .addChild((MapEntryNode) data)
402 nnWriter.write(data);
405 } catch (final IOException e) {
406 LOG.warn("Error writing error response body.", e);
409 return outStream.toString(StandardCharsets.UTF_8);
412 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
413 final ContainerNode data) throws IOException {
414 final QName name = SchemaContext.NAME;
416 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
417 for (final DataContainerChild child : data.body()) {
418 nnWriter.write(child);
421 xmlWriter.writeEndElement();
423 } catch (final XMLStreamException e) {
424 throw new IOException("Failed to write elements", e);
428 private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
429 final ContainerNode data) throws IOException {
430 for (final DataContainerChild child : data.body()) {
431 nnWriter.write(child);