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.errors.RestconfDocumentedException;
39 import org.opendaylight.restconf.common.errors.RestconfError;
40 import org.opendaylight.yangtools.yang.common.QName;
41 import org.opendaylight.yangtools.yang.common.XMLNamespace;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
44 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
45 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
46 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
47 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
48 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
49 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
50 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
51 import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
52 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
53 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
54 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
55 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
56 import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
57 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
58 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
59 import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders;
60 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
62 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
63 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
65 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
66 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
71 * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations
72 * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft.
74 * @author Thomas Pantelis
77 public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
79 private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
81 private static final XMLOutputFactory XML_FACTORY;
84 XML_FACTORY = XMLOutputFactory.newFactory();
85 XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
89 private HttpHeaders headers;
91 private final ControllerContext controllerContext;
93 public RestconfDocumentedExceptionMapper(final ControllerContext controllerContext) {
94 this.controllerContext = requireNonNull(controllerContext);
98 public Response toResponse(final RestconfDocumentedException exception) {
100 LOG.debug("In toResponse: {}", exception.getMessage());
102 final List<MediaType> mediaTypeList = new ArrayList<>();
103 if (headers.getMediaType() != null) {
104 mediaTypeList.add(headers.getMediaType());
107 mediaTypeList.addAll(headers.getAcceptableMediaTypes());
108 final MediaType mediaType = mediaTypeList.stream().filter(type -> !type.equals(MediaType.WILDCARD_TYPE))
109 .findFirst().orElse(MediaType.APPLICATION_JSON_TYPE);
111 LOG.debug("Using MediaType: {}", mediaType);
113 final List<RestconfError> errors = exception.getErrors();
114 if (errors.isEmpty()) {
115 // We don't actually want to send any content but, if we don't set any content here,
116 // the tomcat front-end will send back an html error report. To prevent that, set a
117 // single space char in the entity.
119 return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build();
122 final Status status = ErrorTags.statusOf(errors.iterator().next().getErrorTag());
123 final DataNodeContainer errorsSchemaNode =
124 (DataNodeContainer) controllerContext.getRestconfModuleErrorsSchemaNode();
125 if (errorsSchemaNode == null) {
126 return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build();
129 checkState(errorsSchemaNode instanceof ContainerSchemaNode, "Found Errors SchemaNode isn't ContainerNode");
130 final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> errContBuild =
131 SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) errorsSchemaNode);
133 final List<DataSchemaNode> schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode,
134 Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE);
135 final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null);
136 checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode");
137 final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listErorsBuilder = SchemaAwareBuilders
138 .mapBuilder((ListSchemaNode) errListSchemaNode);
141 for (final RestconfError error : errors) {
142 listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode));
144 errContBuild.withChild(listErorsBuilder.build());
146 final NormalizedNodeContext errContext = new NormalizedNodeContext(InstanceIdentifierContext.ofDataSchemaNode(
147 controllerContext.getGlobalSchema(), (DataSchemaNode) errorsSchemaNode, null), errContBuild.build());
150 if (mediaType.getSubtype().endsWith("json")) {
151 responseBody = toJsonResponseBody(errContext, errorsSchemaNode);
153 responseBody = toXMLResponseBody(errContext, errorsSchemaNode);
156 return Response.status(status).type(mediaType).entity(responseBody).build();
159 private static MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) {
160 checkArgument(errListSchemaNode instanceof ListSchemaNode,
161 "errListSchemaNode has to be of type ListSchemaNode");
162 final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode;
163 final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> errNodeValues = SchemaAwareBuilders
164 .mapEntryBuilder(listStreamSchemaNode);
166 List<DataSchemaNode> lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
167 listStreamSchemaNode, "error-type");
168 final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
169 checkState(errTypSchemaNode instanceof LeafSchemaNode);
170 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTypSchemaNode)
171 .withValue(error.getErrorType().elementBody()).build());
173 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
174 listStreamSchemaNode, "error-tag");
175 final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
176 checkState(errTagSchemaNode instanceof LeafSchemaNode);
177 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errTagSchemaNode)
178 .withValue(error.getErrorTag().elementBody()).build());
180 if (error.getErrorAppTag() != null) {
181 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
182 listStreamSchemaNode, "error-app-tag");
183 final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
184 checkState(errAppTagSchemaNode instanceof LeafSchemaNode);
185 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode)
186 .withValue(error.getErrorAppTag()).build());
189 lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName(
190 listStreamSchemaNode, "error-message");
191 final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null);
192 checkState(errMsgSchemaNode instanceof LeafSchemaNode);
193 errNodeValues.withChild(SchemaAwareBuilders.leafBuilder((LeafSchemaNode) errMsgSchemaNode)
194 .withValue(error.getErrorMessage()).build());
196 if (error.getErrorInfo() != null) {
197 // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
198 // intention is for implementors to define their own data content so we'll just treat it as a leaf
200 errNodeValues.withChild(ImmutableNodes.leafNode(Draft02.RestConfModule.ERROR_INFO_QNAME,
201 error.getErrorInfo()));
204 // TODO : find how could we add possible "error-path"
206 return errNodeValues.build();
209 private static Object toJsonResponseBody(final NormalizedNodeContext errorsNode,
210 final DataNodeContainer errorsSchemaNode) {
211 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
212 NormalizedNode data = errorsNode.getData();
213 final InstanceIdentifierContext context = errorsNode.getInstanceIdentifierContext();
214 final DataSchemaNode schema = (DataSchemaNode) context.getSchemaNode();
216 SchemaPath path = context.getSchemaNode().getPath();
217 final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8);
219 throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
222 boolean isDataRoot = false;
223 XMLNamespace initialNs = null;
224 if (SchemaPath.ROOT.equals(path)) {
227 path = path.getParent();
228 // FIXME: Add proper handling of reading root.
230 if (!schema.isAugmenting() && !(schema instanceof SchemaContext)) {
231 initialNs = schema.getQName().getNamespace();
234 final JsonWriter jsonWriter = JsonWriterFactory.createJsonWriter(outputWriter);
235 final NormalizedNodeStreamWriter jsonStreamWriter = JSONNormalizedNodeStreamWriter.createExclusiveWriter(
236 JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context.getSchemaContext()), path,
237 initialNs, jsonWriter);
239 // We create a delegating writer to special-case error-info as error-info is defined as an empty
240 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
241 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
242 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
244 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
245 private boolean inOurLeaf;
248 protected NormalizedNodeStreamWriter delegate() {
249 return jsonStreamWriter;
253 public void startLeafNode(final NodeIdentifier name) throws IOException {
254 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
256 jsonWriter.name(Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName());
258 super.startLeafNode(name);
263 public void scalarValue(final Object value) throws IOException {
265 jsonWriter.value(value.toString());
267 super.scalarValue(value);
272 public void endNode() throws IOException {
281 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
284 writeDataRoot(outputWriter,nnWriter,(ContainerNode) data);
286 if (data instanceof MapEntryNode) {
287 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
288 .withChild((MapEntryNode) data)
291 nnWriter.write(data);
294 outputWriter.flush();
295 } catch (final IOException e) {
296 LOG.warn("Error writing error response body", e);
300 streamWriter.close();
301 } catch (IOException e) {
302 LOG.warn("Failed to close stream writer", e);
305 return outStream.toString(StandardCharsets.UTF_8);
308 private static Object toXMLResponseBody(final NormalizedNodeContext errorsNode,
309 final DataNodeContainer errorsSchemaNode) {
310 final InstanceIdentifierContext pathContext = errorsNode.getInstanceIdentifierContext();
311 final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
313 final XMLStreamWriter xmlWriter;
315 xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, StandardCharsets.UTF_8.name());
316 } catch (final XMLStreamException | FactoryConfigurationError e) {
317 throw new IllegalStateException(e);
319 NormalizedNode data = errorsNode.getData();
320 SchemaPath schemaPath = pathContext.getSchemaNode().getPath();
322 boolean isDataRoot = false;
323 if (SchemaPath.ROOT.equals(schemaPath)) {
326 schemaPath = schemaPath.getParent();
329 final NormalizedNodeStreamWriter xmlStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
330 pathContext.getSchemaContext(), schemaPath);
332 // We create a delegating writer to special-case error-info as error-info is defined as an empty
333 // container in the restconf yang schema but we create a leaf node so we can output it. The delegate
334 // stream writer validates the node type against the schema and thus will expect a LeafSchemaNode but
335 // the schema has a ContainerSchemaNode so, to avoid an error, we override the leafNode behavior
337 final NormalizedNodeStreamWriter streamWriter = new ForwardingNormalizedNodeStreamWriter() {
338 private boolean inOurLeaf;
341 protected NormalizedNodeStreamWriter delegate() {
342 return xmlStreamWriter;
346 public void startLeafNode(final NodeIdentifier name) throws IOException {
347 if (name.getNodeType().equals(Draft02.RestConfModule.ERROR_INFO_QNAME)) {
348 String ns = Draft02.RestConfModule.ERROR_INFO_QNAME.getNamespace().toString();
350 xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX,
351 Draft02.RestConfModule.ERROR_INFO_QNAME.getLocalName(), ns);
352 } catch (XMLStreamException e) {
353 throw new IOException("Error writing error-info", e);
357 super.startLeafNode(name);
362 public void scalarValue(final Object value) throws IOException {
365 xmlWriter.writeCharacters(value.toString());
366 } catch (XMLStreamException e) {
367 throw new IOException("Error writing error-info", e);
370 super.scalarValue(value);
375 public void endNode() throws IOException {
378 xmlWriter.writeEndElement();
379 } catch (XMLStreamException e) {
380 throw new IOException("Error writing error-info", e);
389 final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter);
392 writeRootElement(xmlWriter, nnWriter, (ContainerNode) data);
394 if (data instanceof MapEntryNode) {
395 // Restconf allows returning one list item. We need to wrap it
396 // in map node in order to serialize it properly
397 data = ImmutableNodes.mapNodeBuilder(data.getIdentifier().getNodeType())
398 .addChild((MapEntryNode) data)
401 nnWriter.write(data);
404 } catch (final IOException e) {
405 LOG.warn("Error writing error response body.", e);
408 return outStream.toString(StandardCharsets.UTF_8);
411 private static void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter,
412 final ContainerNode data) throws IOException {
413 final QName name = SchemaContext.NAME;
415 xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName());
416 for (final DataContainerChild child : data.body()) {
417 nnWriter.write(child);
420 xmlWriter.writeEndElement();
422 } catch (final XMLStreamException e) {
423 throw new IOException("Failed to write elements", e);
427 private static void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter,
428 final ContainerNode data) throws IOException {
429 for (final DataContainerChild child : data.body()) {
430 nnWriter.write(child);