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.gson.stream.JsonWriter;
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.OutputStreamWriter;
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.core.Response.Status;
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.ErrorTags;
36 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
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.common.XMLNamespace;
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.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.NormalizedNode;
47 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
49 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
50 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
51 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
52 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
53 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
54 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
55 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
56 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
57 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
58 import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders;
59 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
67 * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
68 * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
70 * @author Thomas Pantelis
73 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
75 private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
77 private static final XMLOutputFactory XML_FACTORY;
80 XML_FACTORY = XMLOutputFactory.newFactory();
81 XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
85 private HttpHeaders headers;
87 private final ControllerContext controllerContext;
89 public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
90 this.controllerContext = requireNonNull(controllerContext);
94 public Response toResponse(final RestconfDocumentedException exception) {
96 LOG.debug("In toResponse: {}", exception.getMessage());
98 final List<MediaType> mediaTypeList = new ArrayList<>();
99 if (headers.getMediaType() != null) {
100 mediaTypeList.add(headers.getMediaType());
103 mediaTypeList.addAll(headers.getAcceptableMediaTypes());
104 final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
105 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
107 LOG.debug("Using MediaType: {}", mediaType);
109 final List<RestconfError> errors = exception.getErrors();
110 if (errors.isEmpty()) {
111 // We don't actually want to send any content but, if we don't set any content here,
112 // the tomcat front-end will send back an html error report. To prevent that, set a
113 // single space char in the entity.
115 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
118 final Status status = ErrorTags.statusOf(errors.iterator().next().getErrorTag());
119 final var errorsEntry = controllerContext.getRestconfModuleErrorsSchemaNode();
120 if (errorsEntry == null) {
121 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
124 final var errorsSchemaNode = errorsEntry.getValue();
125 final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
126 SchemaAwareBuilders.containerBuilder(errorsSchemaNode);
128 final var schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
129 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
130 final DataSchemaNode errListSchemaNode = ControllerContext.getFirst(schemaList);
131 checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
132 final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listErorsBuilder = SchemaAwareBuilders
133 .mapBuilder((ListSchemaNode) errListSchemaNode);
136 for (final RestconfError error : errors) {
137 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
139 errContBuild.withChild(listErorsBuilder.build());
141 final NormalizedNodeContext errContext = new NormalizedNodeContext(
142 InstanceIdentifierContext.ofStack(errorsEntry.getKey(), null), errContBuild.build());
144 final String responseBody;
145 if (mediaType.getSubtype().endsWith("json")) {
146 responseBody = toJsonResponseBody(errContext);
148 responseBody = toXMLResponseBody(errContext);
151 return Response.status(status).type(mediaType).entity(responseBody).build();
154 private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
155 checkArgument(errListSchemaNode instanceof ListSchemaNode,
156 "errListSchemaNode has to be of type ListSchemaNode");
157 final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
158 final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = SchemaAwareBuilders
159 .mapEntryBuilder(listStreamSchemaNode);
161 var lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
162 listStreamSchemaNode, "error-type");
163 final DataSchemaNode errTypSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode);
164 checkState(errTypSchemaNode instanceof LeafSchemaNode);
165 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
166 .withValue(error.getErrorType().elementBody()).build());
168 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
169 listStreamSchemaNode, "error-tag");
170 final DataSchemaNode errTagSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode);
171 checkState(errTagSchemaNode instanceof LeafSchemaNode);
172 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
173 .withValue(error.getErrorTag().elementBody()).build());
175 if (error.getErrorAppTag() != null) {
176 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
177 listStreamSchemaNode, "error-app-tag");
178 final DataSchemaNode errAppTagSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode);
179 checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
180 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
181 .withValue(error.getErrorAppTag()).build());
184 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
185 listStreamSchemaNode, "error-message");
186 final DataSchemaNode errMsgSchemaNode = ControllerContext.getFirst(lsChildDataSchemaNode);
187 checkState(errMsgSchemaNode instanceof LeafSchemaNode);
188 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
189 .withValue(error.getErrorMessage()).build());
191 if (error.getErrorInfo() != null) {
192 // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
193 // intention is for implementors to define their own data content so we'll just treat it as a leaf
195 errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
196 error.getErrorInfo()));
199 // TODO : find how could we add possible "error-path"
201 return errNodeValues.build();
204 private static String toJsonResponseBody(final NormalizedNodeContext errorsNode) {
205 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
206 NormalizedNode data = errorsNode.getData();
207 final InstanceIdentifierContext context = errorsNode.getInstanceIdentifierContext();
208 final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
210 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
212 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
215 final boolean isDataRoot;
216 final var stack = context.inference().toSchemaInferenceStack();
217 if (stack.isEmpty()) {
222 // FIXME: Add proper handling of reading root.
225 XMLNamespace initialNs = null;
226 if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
227 initialNs = schema.getQName().getNamespace();
230 final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
231 final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
232 JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()),
233 stack.toInference(), initialNs, jsonWriter);
235 // We create a delegating writer to special-case error-info as error-info is defined as an empty
236 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
237 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
238 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
240 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
241 private boolean inOurLeaf;
244 protected NormalizedNodeStreamWriter delegate() {
245 return jsonStreamWriter;
249 public void startLeafNode(final NodeIdentifier name) throws IOException {
250 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
252 jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
254 super.startLeafNode(name);
259 public void scalarValue(final Object value) throws IOException {
261 jsonWriter.value(value.toString());
263 super.scalarValue(value);
268 public void endNode() throws IOException {
277 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
280 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
282 if (data instanceof MapEntryNode) {
283 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
284 .withChild((MapEntryNode) data)
287 nnWriter.write(data);
290 outputWriter.flush();
291 } catch (final IOException e) {
292 LOG.warn("Error writing error response body", e);
296 streamWriter.close();
297 } catch (IOException e) {
298 LOG.warn("Failed to close stream writer", e);
301 return outStream.toString(StandardCharsets.UTF_8);
304 private static String toXMLResponseBody(final NormalizedNodeContext errorsNode) {
305 final InstanceIdentifierContext pathContext = errorsNode.getInstanceIdentifierContext();
306 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
308 final XMLStreamWriter xmlWriter;
310 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
311 } catch (final XMLStreamException | FactoryConfigurationError e) {
312 throw new IllegalStateException(e);
314 NormalizedNode data = errorsNode.getData();
316 final boolean isDataRoot;
317 final var stack = pathContext.inference().toSchemaInferenceStack();
318 if (stack.isEmpty()) {
325 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
326 stack.toInference());
328 // We create a delegating writer to special-case error-info as error-info is defined as an empty
329 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
330 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
331 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
333 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
334 private boolean inOurLeaf;
337 protected NormalizedNodeStreamWriter delegate() {
338 return xmlStreamWriter;
342 public void startLeafNode(final NodeIdentifier name) throws IOException {
343 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
344 String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
346 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
347 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
348 } catch (XMLStreamException e) {
349 throw new IOException("Error writing error-info", e);
353 super.startLeafNode(name);
358 public void scalarValue(final Object value) throws IOException {
361 xmlWriter.writeCharacters(value.toString());
362 } catch (XMLStreamException e) {
363 throw new IOException("Error writing error-info", e);
366 super.scalarValue(value);
371 public void endNode() throws IOException {
374 xmlWriter.writeEndElement();
375 } catch (XMLStreamException e) {
376 throw new IOException("Error writing error-info", e);
385 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
388 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
390 if (data instanceof MapEntryNode) {
391 // Restconf allows returning one list item. We need to wrap it
392 // in map node in order to serialize it properly
393 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
394 .addChild((MapEntryNode) data)
397 nnWriter.write(data);
400 } catch (final IOException e) {
401 LOG.warn("Error writing error response body.", e);
404 return outStream.toString(StandardCharsets.UTF_8);
407 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
408 final ContainerNode data) throws IOException {
409 final QName name = SchemaContext.NAME;
411 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
412 for (final DataContainerChild child : data.body()) {
413 nnWriter.write(child);
416 xmlWriter.writeEndElement();
418 } catch (final XMLStreamException e) {
419 throw new IOException("Failed to write elements", e);
423 private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
424 final ContainerNode data) throws IOException {
425 for (final DataContainerChild child : data.body()) {
426 nnWriter.write(child);