X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=blobdiff_plain;f=opendaylight%2Fmd-sal%2Fsal-rest-connector%2Fsrc%2Fmain%2Fjava%2Forg%2Fopendaylight%2Fcontroller%2Fsal%2Frest%2Fimpl%2FRestconfDocumentedExceptionMapper.java;h=f52b42337e2a759ed52b01f9c85aa5bd5fba227f;hp=0854ca71673465e76e7659bfa9479afc3fb1962f;hb=06e889c9c78457590b6a0b62d89a6b9f44242a9f;hpb=7e24111a0842d66187c752022aa975c411b42cca diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java index 0854ca7167..f52b42337e 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java @@ -8,270 +8,305 @@ package org.opendaylight.controller.sal.rest.impl; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERRORS_CONTAINER_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_APP_TAG_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_INFO_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_LIST_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_MESSAGE_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_TAG_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.ERROR_TYPE_QNAME; -import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.NAMESPACE; - import com.google.common.base.Charsets; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.gson.stream.JsonWriter; - +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; -import java.io.StringReader; -import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.Iterator; import java.util.List; -import java.util.Map.Entry; - -import javax.activation.UnsupportedDataTypeException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.TransformerFactoryConfigurationError; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +import org.opendaylight.controller.sal.restconf.impl.InstanceIdentifierContext; +import org.opendaylight.controller.sal.restconf.impl.NormalizedNodeContext; import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.controller.sal.restconf.impl.RestconfError; import org.opendaylight.yangtools.yang.common.QName; -import org.opendaylight.yangtools.yang.data.api.CompositeNode; -import org.opendaylight.yangtools.yang.data.api.Node; -import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode; -import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils; -import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder; +import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaPath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.xml.sax.InputSource; /** - * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by - * resource implementations and translates appropriately to restconf error response as defined in - * the RESTCONF RFC draft. + * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by resource implementations + * and translates appropriately to restconf error response as defined in the RESTCONF RFC draft. * * @author Thomas Pantelis */ @Provider public class RestconfDocumentedExceptionMapper implements ExceptionMapper { - private final static Logger LOG = LoggerFactory.getLogger( RestconfDocumentedExceptionMapper.class ); + private final static Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class); + + private static final XMLOutputFactory XML_FACTORY; + + static { + XML_FACTORY = XMLOutputFactory.newFactory(); + XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); + } @Context private HttpHeaders headers; @Override - public Response toResponse( final RestconfDocumentedException exception ) { - - LOG.debug( "In toResponse: {}", exception.getMessage() ); - - // Default to the content type if there's no Accept header + public Response toResponse(final RestconfDocumentedException exception) { - MediaType mediaType = headers.getMediaType(); + LOG.debug("In toResponse: {}", exception.getMessage()); - List accepts = headers.getAcceptableMediaTypes(); + final List accepts = headers.getAcceptableMediaTypes(); + accepts.remove(MediaType.WILDCARD_TYPE); - LOG.debug( "Accept headers: {}", accepts ); + LOG.debug("Accept headers: {}", accepts); - if( accepts != null && accepts.size() > 0 ) { - mediaType = accepts.get( 0 ); // just pick the first one + final MediaType mediaType; + if (accepts != null && accepts.size() > 0) { + mediaType = accepts.get(0); // just pick the first one + } else { + // Default to the content type if there's no Accept header + mediaType = MediaType.APPLICATION_JSON_TYPE; } - LOG.debug( "Using MediaType: {}", mediaType ); + LOG.debug("Using MediaType: {}", mediaType); - List errors = exception.getErrors(); - if( errors.isEmpty() ) { + final List errors = exception.getErrors(); + if (errors.isEmpty()) { // We don't actually want to send any content but, if we don't set any content here, // the tomcat front-end will send back an html error report. To prevent that, set a // single space char in the entity. - return Response.status( exception.getStatus() ) - .type( MediaType.TEXT_PLAIN_TYPE ) - .entity( " " ).build(); + return Response.status(exception.getStatus()).type(MediaType.TEXT_PLAIN_TYPE).entity(" ").build(); } - int status = errors.iterator().next().getErrorTag().getStatusCode(); + final int status = errors.iterator().next().getErrorTag().getStatusCode(); - ControllerContext context = ControllerContext.getInstance(); - DataNodeContainer errorsSchemaNode = (DataNodeContainer)context.getRestconfModuleErrorsSchemaNode(); + final ControllerContext context = ControllerContext.getInstance(); + final DataNodeContainer errorsSchemaNode = (DataNodeContainer) context.getRestconfModuleErrorsSchemaNode(); - if( errorsSchemaNode == null ) { - return Response.status( status ) - .type( MediaType.TEXT_PLAIN_TYPE ) - .entity( exception.getMessage() ).build(); + if (errorsSchemaNode == null) { + return Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(exception.getMessage()).build(); } - ImmutableList.Builder> errorNodes = ImmutableList.> builder(); - for( RestconfError error: errors ) { - errorNodes.add( toDomNode( error ) ); + Preconditions.checkState(errorsSchemaNode instanceof ContainerSchemaNode, + "Found Errors SchemaNode isn't ContainerNode"); + final DataContainerNodeAttrBuilder errContBuild = + Builders.containerBuilder((ContainerSchemaNode) errorsSchemaNode); + + final List schemaList = ControllerContext.findInstanceDataChildrenByName(errorsSchemaNode, + Draft02.RestConfModule.ERROR_LIST_SCHEMA_NODE); + final DataSchemaNode errListSchemaNode = Iterables.getFirst(schemaList, null); + Preconditions.checkState(errListSchemaNode instanceof ListSchemaNode, "Found Error SchemaNode isn't ListSchemaNode"); + final CollectionNodeBuilder listErorsBuilder = Builders + .mapBuilder((ListSchemaNode) errListSchemaNode); + + + for (final RestconfError error : errors) { + listErorsBuilder.withChild(toErrorEntryNode(error, errListSchemaNode)); } + errContBuild.withChild(listErorsBuilder.build()); - ImmutableCompositeNode errorsNode = - ImmutableCompositeNode.create( ERRORS_CONTAINER_QNAME, errorNodes.build() ); + final NormalizedNodeContext errContext = new NormalizedNodeContext(new InstanceIdentifierContext(null, + (DataSchemaNode) errorsSchemaNode, null, context.getGlobalSchema()), errContBuild.build()); Object responseBody; - if( mediaType.getSubtype().endsWith( "json" ) ) { - responseBody = toJsonResponseBody( errorsNode, errorsSchemaNode ); - } - else { - responseBody = toXMLResponseBody( errorsNode, errorsSchemaNode ); + if (mediaType.getSubtype().endsWith("json")) { + responseBody = toJsonResponseBody(errContext, errorsSchemaNode); + } else { + responseBody = toXMLResponseBody(errContext, errorsSchemaNode); } - return Response.status( status ).type( mediaType ).entity( responseBody ).build(); + return Response.status(status).type(mediaType).entity(responseBody).build(); } - private Object toJsonResponseBody( final ImmutableCompositeNode errorsNode, - final DataNodeContainer errorsSchemaNode ) { - - JsonMapper jsonMapper = new JsonMapper(null); - - Object responseBody = null; - try { - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - JsonWriter writer = new JsonWriter(new OutputStreamWriter( outStream, Charsets.UTF_8)); - writer.setIndent( " " ); + private MapEntryNode toErrorEntryNode(final RestconfError error, final DataSchemaNode errListSchemaNode) { + Preconditions.checkArgument(errListSchemaNode instanceof ListSchemaNode, + "errListSchemaNode has to be of type ListSchemaNode"); + final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) errListSchemaNode; + final DataContainerNodeAttrBuilder errNodeValues = Builders + .mapEntryBuilder(listStreamSchemaNode); + + List lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + (listStreamSchemaNode), "error-type"); + final DataSchemaNode errTypSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null); + Preconditions.checkState(errTypSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTypSchemaNode) + .withValue(error.getErrorType().getErrorTypeTag()).build()); + + lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + (listStreamSchemaNode), "error-tag"); + final DataSchemaNode errTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null); + Preconditions.checkState(errTagSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errTagSchemaNode) + .withValue(error.getErrorTag().getTagValue()).build()); + + if (error.getErrorAppTag() != null) { + lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + (listStreamSchemaNode), "error-app-tag"); + final DataSchemaNode errAppTagSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null); + Preconditions.checkState(errAppTagSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errAppTagSchemaNode) + .withValue(error.getErrorAppTag()).build()); + } - jsonMapper.write( writer, errorsNode, errorsSchemaNode); - writer.flush(); + lsChildDataSchemaNode = ControllerContext.findInstanceDataChildrenByName( + (listStreamSchemaNode), "error-message"); + final DataSchemaNode errMsgSchemaNode = Iterables.getFirst(lsChildDataSchemaNode, null); + Preconditions.checkState(errMsgSchemaNode instanceof LeafSchemaNode); + errNodeValues.withChild(Builders.leafBuilder((LeafSchemaNode) errMsgSchemaNode) + .withValue(error.getErrorMessage()).build()); - responseBody = outStream.toString( "UTF-8" ); - } catch( IOException e ) { - LOG.error( "Error writing error response body", e ); - } + // TODO : find how could we add possible "error-path" and "error-info" - return responseBody; + return errNodeValues.build(); } - private Object toXMLResponseBody( final ImmutableCompositeNode errorsNode, - final DataNodeContainer errorsSchemaNode ) { + private Object toJsonResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) { - XmlMapper xmlMapper = new XmlMapper(); + final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + NormalizedNode data = errorsNode.getData(); + final InstanceIdentifierContext context = (InstanceIdentifierContext) errorsNode.getInstanceIdentifierContext(); + final DataSchemaNode schema = context.getSchemaNode(); - Object responseBody = null; - try { - Document xmlDoc = xmlMapper.write( errorsNode, errorsSchemaNode ); + SchemaPath path = context.getSchemaNode().getPath(); + final OutputStreamWriter outputWriter = new OutputStreamWriter(outStream, Charsets.UTF_8); + if (data == null) { + throw new RestconfDocumentedException(Response.Status.NOT_FOUND); + } - responseBody = documentToString( xmlDoc ); + boolean isDataRoot = false; + URI initialNs = null; + if (SchemaPath.ROOT.equals(path)) { + isDataRoot = true; + } else { + path = path.getParent(); + // FIXME: Add proper handling of reading root. } - catch( TransformerException | UnsupportedDataTypeException | UnsupportedEncodingException e ) { - LOG.error( "Error writing error response body", e ); + if(!schema.isAugmenting() && !(schema instanceof SchemaContext)) { + initialNs = schema.getQName().getNamespace(); + } + final NormalizedNodeStreamWriter jsonWriter = JSONNormalizedNodeStreamWriter.create(context.getSchemaContext(),path,initialNs,outputWriter); + final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(jsonWriter); + try { + if(isDataRoot) { + writeDataRoot(outputWriter,nnWriter,(ContainerNode) data); + } else { + if(data instanceof MapEntryNode) { + data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).withChild(((MapEntryNode) data)).build(); + } + nnWriter.write(data); + } + nnWriter.flush(); + outputWriter.flush(); + } + catch (final IOException e) { + LOG.warn("Error writing error response body", e); } - return responseBody; - } - - private String documentToString( final Document doc ) throws TransformerException, UnsupportedEncodingException { - Transformer transformer = createTransformer(); - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - - transformer.transform( new DOMSource( doc ), new StreamResult( outStream ) ); - - return outStream.toString( "UTF-8" ); - } + return outStream.toString(); - private Transformer createTransformer() throws TransformerFactoryConfigurationError, - TransformerConfigurationException { - TransformerFactory tf = TransformerFactory.newInstance(); - Transformer transformer = tf.newTransformer(); - transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "no" ); - transformer.setOutputProperty( OutputKeys.METHOD, "xml" ); - transformer.setOutputProperty( OutputKeys.INDENT, "yes" ); - transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" ); - transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "4" ); - return transformer; } - private Node toDomNode( final RestconfError error ) { + private Object toXMLResponseBody(final NormalizedNodeContext errorsNode, final DataNodeContainer errorsSchemaNode) { - CompositeNodeBuilder builder = ImmutableCompositeNode.builder(); - builder.setQName( ERROR_LIST_QNAME ); + final InstanceIdentifierContext pathContext = (InstanceIdentifierContext) errorsNode.getInstanceIdentifierContext(); + final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - addLeaf( builder, ERROR_TYPE_QNAME, error.getErrorType().getErrorTypeTag() ); - addLeaf( builder, ERROR_TAG_QNAME, error.getErrorTag().getTagValue() ); - addLeaf( builder, ERROR_MESSAGE_QNAME, error.getErrorMessage() ); - addLeaf( builder, ERROR_APP_TAG_QNAME, error.getErrorAppTag() ); - - Node errorInfoNode = parseErrorInfo( error.getErrorInfo() ); - if( errorInfoNode != null ) { - builder.add( errorInfoNode ); + XMLStreamWriter xmlWriter; + try { + xmlWriter = XML_FACTORY.createXMLStreamWriter(outStream, "UTF-8"); + } catch (final XMLStreamException e) { + throw new IllegalStateException(e); + } catch (final FactoryConfigurationError e) { + throw new IllegalStateException(e); } - - return builder.toInstance(); - } - - private Node parseErrorInfo( final String errorInfo ) { - if( Strings.isNullOrEmpty( errorInfo ) ) { - return null; + NormalizedNode data = errorsNode.getData(); + SchemaPath schemaPath = pathContext.getSchemaNode().getPath(); + + boolean isDataRoot = false; + if (SchemaPath.ROOT.equals(schemaPath)) { + isDataRoot = true; + } else { + schemaPath = schemaPath.getParent(); } - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware( true ); - factory.setCoalescing( true ); - factory.setIgnoringElementContentWhitespace( true ); - factory.setIgnoringComments( true ); - - // Wrap the error info content in a root element so it can be parsed - // as XML. The error info content may or may not be XML. If not then it will be - // parsed as text content of the element. - - String errorInfoWithRoot = - new StringBuilder( "" ) - .append( errorInfo ).append( "" ).toString(); - - Document doc = null; + final NormalizedNodeStreamWriter streamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, + pathContext.getSchemaContext(), schemaPath); + final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter); try { - doc = factory.newDocumentBuilder().parse( - new InputSource( new StringReader( errorInfoWithRoot ) ) ); - } catch( Exception e ) { - // TODO: what if the content is text that happens to contain invalid markup? Could - // wrap in CDATA and try again. - - LOG.warn( "Error parsing restconf error-info, \"{}\", as XML", errorInfo, e); - return null; + if (isDataRoot) { + writeRootElement(xmlWriter, nnWriter, (ContainerNode) data); + } else { + if (data instanceof MapEntryNode) { + // Restconf allows returning one list item. We need to wrap it + // in map node in order to serialize it properly + data = ImmutableNodes.mapNodeBuilder(data.getNodeType()).addChild((MapEntryNode) data).build(); + } + nnWriter.write(data); + nnWriter.flush(); + } + } + catch (final IOException e) { + LOG.warn("Error writing error response body.", e); } - Node errorInfoNode = XmlDocumentUtils.toDomNode( doc ); - - if( errorInfoNode instanceof CompositeNode ) { - CompositeNode compositeNode = (CompositeNode)XmlDocumentUtils.toDomNode( doc ); - - // At this point the QName for the "error-info" CompositeNode doesn't contain the revision - // as it isn't present in the XML. So we'll copy all the child nodes and create a new - // CompositeNode with the full QName. This is done so the XML/JSON mapping code can - // locate the schema. + return outStream.toString(); + } - ImmutableList.Builder> childNodes = ImmutableList.builder(); - for( Entry>> entry: compositeNode.entrySet() ) { - childNodes.addAll( entry.getValue() ); + private void writeRootElement(final XMLStreamWriter xmlWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data) + throws IOException { + try { + final QName name = SchemaContext.NAME; + xmlWriter.writeStartElement(name.getNamespace().toString(), name.getLocalName()); + for (final DataContainerChild child : data.getValue()) { + nnWriter.write(child); } - - errorInfoNode = ImmutableCompositeNode.create( ERROR_INFO_QNAME, childNodes.build() ); + nnWriter.flush(); + xmlWriter.writeEndElement(); + xmlWriter.flush(); + } catch (final XMLStreamException e) { + Throwables.propagate(e); } - - return errorInfoNode; } - private void addLeaf( final CompositeNodeBuilder builder, final QName qname, - final String value ) { - if( !Strings.isNullOrEmpty( value ) ) { - builder.addLeaf( qname, value ); + private void writeDataRoot(final OutputStreamWriter outputWriter, final NormalizedNodeWriter nnWriter, final ContainerNode data) throws IOException { + final Iterator> iterator = data.getValue().iterator(); + while(iterator.hasNext()) { + final DataContainerChild child = iterator.next(); + nnWriter.write(child); + nnWriter.flush(); } } }