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;
13 import static org.opendaylight.netconf.sal.rest.api.Draft02.RestConfModule.ERRORS_CONTAINER_QNAME;
15 import com.google.common.collect.Iterables;
16 import com.google.gson.stream.JsonWriter;
17 import java.io.ByteArrayOutputStream;
18 import java.io.IOException;
19 import java.io.OutputStreamWriter;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.stream.Collectors;
24 import javax.ws.rs.core.Context;
25 import javax.ws.rs.core.HttpHeaders;
26 import javax.ws.rs.core.MediaType;
27 import javax.ws.rs.core.Response;
28 import javax.ws.rs.core.Response.Status;
29 import javax.ws.rs.ext.ExceptionMapper;
30 import javax.ws.rs.ext.Provider;
31 import javax.xml.XMLConstants;
32 import javax.xml.stream.FactoryConfigurationError;
33 import javax.xml.stream.XMLOutputFactory;
34 import javax.xml.stream.XMLStreamException;
35 import javax.xml.stream.XMLStreamWriter;
36 import org.opendaylight.netconf.sal.rest.api.Draft02.RestConfModule;
37 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
38 import org.opendaylight.restconf.common.ErrorTags;
39 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
40 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
41 import org.opendaylight.restconf.common.errors.RestconfError;
42 import org.opendaylight.yangtools.yang.common.QName;
43 import org.opendaylight.yangtools.yang.common.XMLNamespace;
44 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
45 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
46 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
47 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
48 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
49 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
50 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
51 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
52 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
53 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
54 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
55 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
56 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
57 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
58 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
59 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
60 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
61 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
62 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
63 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
64 import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders;
65 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
66 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
67 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
68 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
69 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
70 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
71 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
72 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
77 * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
78 * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
80 * @author Thomas Pantelis
83 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
85 private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
87 private static final XMLOutputFactory XML_FACTORY;
89 private static final YangInstanceIdentifier ERRORS = YangInstanceIdentifier.builder()
90 .node(ERRORS_CONTAINER_QNAME)
91 .node(ERRORS_CONTAINER_QNAME)
95 XML_FACTORY = XMLOutputFactory.newFactory();
96 XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
100 private HttpHeaders headers;
102 private final ControllerContext controllerContext;
104 public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
105 this.controllerContext = requireNonNull(controllerContext);
109 public Response toResponse(final RestconfDocumentedException exception) {
111 LOG.debug("In toResponse: {}", exception.getMessage());
113 final List<MediaType> mediaTypeList = new ArrayList<>();
114 if (headers.getMediaType() != null) {
115 mediaTypeList.add(headers.getMediaType());
118 mediaTypeList.addAll(headers.getAcceptableMediaTypes());
119 final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
120 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
122 LOG.debug("Using MediaType: {}", mediaType);
124 final List<RestconfError> errors = exception.getErrors();
125 if (errors.isEmpty()) {
126 // We don't actually want to send any content but, if we don't set any content here,
127 // the tomcat front-end will send back an html error report. To prevent that, set a
128 // single space char in the entity.
130 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
133 final Status status = ErrorTags.statusOf(errors.iterator().next().getErrorTag());
134 final DataNodeContainer errorsSchemaNode =
135 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
136 if (errorsSchemaNode == null) {
137 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
140 checkState(errorsSchemaNode instanceof ContainerSchemaNode, "Found Errors SchemaNode isn't ContainerNode");
141 final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
142 SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
144 final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
145 RestConfModule.ERROR_LIST_SCHEMA_NODE);
146 final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
147 checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
148 final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listErorsBuilder = SchemaAwareBuilders
149 .mapBuilder((ListSchemaNode) errListSchemaNode);
152 for (final RestconfError error : errors) {
153 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
155 errContBuild.withChild(listErorsBuilder.build());
157 final NormalizedNodeContext errContext = new NormalizedNodeContext(new InstanceIdentifierContext<>(ERRORS,
158 (DataSchemaNode) errorsSchemaNode, null, controllerContext.getGlobalSchema()), errContBuild.build());
161 if (mediaType.getSubtype().endsWith("json")) {
162 responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
164 responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
167 return Response.status(status).type(mediaType).entity(responseBody).build();
170 private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
171 checkArgument(errListSchemaNode instanceof ListSchemaNode,
172 "errListSchemaNode has to be of type ListSchemaNode");
173 final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
174 final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = SchemaAwareBuilders
175 .mapEntryBuilder(listStreamSchemaNode);
177 List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
178 listStreamSchemaNode, "error-type");
179 final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
180 checkState(errTypSchemaNode instanceof LeafSchemaNode);
181 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
182 .withValue(error.getErrorType().elementBody()).build());
184 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
185 listStreamSchemaNode, "error-tag");
186 final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
187 checkState(errTagSchemaNode instanceof LeafSchemaNode);
188 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
189 .withValue(error.getErrorTag().elementBody()).build());
191 if (error.getErrorAppTag() != null) {
192 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
193 listStreamSchemaNode, "error-app-tag");
194 final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
195 checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
196 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
197 .withValue(error.getErrorAppTag()).build());
200 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
201 listStreamSchemaNode, "error-message");
202 final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
203 checkState(errMsgSchemaNode instanceof LeafSchemaNode);
204 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
205 .withValue(error.getErrorMessage()).build());
207 if (error.getErrorInfo() != null) {
208 // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
209 // intention is for implementors to define their own data content so we'll just treat it as a leaf
211 errNodeValues.withChild(ImmutableNodes.leafNode(RestConfModule.ERROR_INFO_QNAME,
212 error.getErrorInfo()));
215 // TODO : find how could we add possible "error-path"
217 return errNodeValues.build();
220 private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode,
221 final DataNodeContainer errorsSchemaNode) {
222 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
223 NormalizedNode data = errorsNode.getData();
224 final InstanceIdentifierContext<?> context = errorsNode.getInstanceIdentifierContext();
225 final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
227 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
229 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
232 boolean isDataRoot = false;
233 XMLNamespace initialNs = null;
235 if (context.getSchemaNode() instanceof SchemaContext) {
237 path = SchemaPath.ROOT;
239 final List<QName> qNames = context.getInstanceIdentifier().getPathArguments().stream()
240 .filter(arg -> !(arg instanceof NodeIdentifierWithPredicates))
241 .filter(arg -> !(arg instanceof AugmentationIdentifier))
242 .map(PathArgument::getNodeType)
243 .collect(Collectors.toList());
244 path = SchemaPath.of(Absolute.of(qNames)).getParent();
246 if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
247 initialNs = schema.getQName().getNamespace();
250 final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
251 final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
252 JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), path,
253 initialNs, jsonWriter);
255 // We create a delegating writer to special-case error-info as error-info is defined as an empty
256 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
257 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
258 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
260 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
261 private boolean inOurLeaf;
264 protected NormalizedNodeStreamWriter delegate() {
265 return jsonStreamWriter;
269 public void startLeafNode(final NodeIdentifier name) throws IOException {
270 if (name.getNodeType().equals(RestConfModule.ERROR_INFO_QNAME)) {
272 jsonWriter.name(RestConfModule.ERROR_INFO_QNAME.getLocalName());
274 super.startLeafNode(name);
279 public void scalarValue(final Object value) throws IOException {
281 jsonWriter.value(value.toString());
283 super.scalarValue(value);
288 public void endNode() throws IOException {
297 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
300 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
302 if (data instanceof MapEntryNode) {
303 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
304 .withChild((MapEntryNode) data)
307 nnWriter.write(data);
310 outputWriter.flush();
311 } catch (final IOException e) {
312 LOG.warn("Error writing error response body", e);
316 streamWriter.close();
317 } catch (IOException e) {
318 LOG.warn("Failed to close stream writer", e);
321 return outStream.toString(StandardCharsets.UTF_8);
324 private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
325 final DataNodeContainer errorsSchemaNode) {
326 final InstanceIdentifierContext<?> pathContext = errorsNode.getInstanceIdentifierContext();
327 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
329 final XMLStreamWriter xmlWriter;
331 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
332 } catch (final XMLStreamException | FactoryConfigurationError e) {
333 throw new IllegalStateException(e);
335 NormalizedNode data = errorsNode.getData();
336 SchemaPath schemaPath;
337 boolean isDataRoot = false;
338 if (pathContext.getSchemaNode() instanceof SchemaContext) {
340 schemaPath = SchemaPath.ROOT;
342 final List<QName> qNames = pathContext.getInstanceIdentifier().getPathArguments().stream()
343 .filter(arg -> !(arg instanceof NodeIdentifierWithPredicates))
344 .filter(arg -> !(arg instanceof AugmentationIdentifier))
345 .map(PathArgument::getNodeType)
346 .collect(Collectors.toList());
347 schemaPath = SchemaPath.of(Absolute.of(qNames)).getParent();
350 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
351 pathContext.getSchemaContext(), schemaPath);
353 // We create a delegating writer to special-case error-info as error-info is defined as an empty
354 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
355 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
356 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
358 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
359 private boolean inOurLeaf;
362 protected NormalizedNodeStreamWriter delegate() {
363 return xmlStreamWriter;
367 public void startLeafNode(final NodeIdentifier name) throws IOException {
368 if (name.getNodeType().equals(RestConfModule.ERROR_INFO_QNAME)) {
369 String ns = RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
371 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
372 RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
373 } catch (XMLStreamException e) {
374 throw new IOException("Error writing error-info", e);
378 super.startLeafNode(name);
383 public void scalarValue(final Object value) throws IOException {
386 xmlWriter.writeCharacters(value.toString());
387 } catch (XMLStreamException e) {
388 throw new IOException("Error writing error-info", e);
391 super.scalarValue(value);
396 public void endNode() throws IOException {
399 xmlWriter.writeEndElement();
400 } catch (XMLStreamException e) {
401 throw new IOException("Error writing error-info", e);
410 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
413 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
415 if (data instanceof MapEntryNode) {
416 // Restconf allows returning one list item. We need to wrap it
417 // in map node in order to serialize it properly
418 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
419 .addChild((MapEntryNode) data)
422 nnWriter.write(data);
425 } catch (final IOException e) {
426 LOG.warn("Error writing error response body.", e);
429 return outStream.toString(StandardCharsets.UTF_8);
432 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
433 final ContainerNode data) throws IOException {
434 final QName name = SchemaContext.NAME;
436 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
437 for (final DataContainerChild child : data.body()) {
438 nnWriter.write(child);
441 xmlWriter.writeEndElement();
443 } catch (final XMLStreamException e) {
444 throw new IOException("Failed to write elements", e);
448 private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
449 final ContainerNode data) throws IOException {
450 for (final DataContainerChild child : data.body()) {
451 nnWriter.write(child);