From 26da3c2a206a753356b507b018052cbb9cccca7d Mon Sep 17 00:00:00 2001 From: tpantelis Date: Thu, 8 May 2014 14:25:02 -0400 Subject: [PATCH] Bug 1010: Implement restconf error responses - RestconfError (new): encapsulates error information as defined in the RESTCON RFC. - RestconfDocumentedException (new): exception that wraps RestconError info. - RestconfDocumentedExceptionMapper (new): JAX-RS ExceptionMapper that translates a RestconfDocumentedException appropriately to XML or JSON depending on the user's mime type. - JsonMapper: modified to handle null child schema as restconf error-info is defined as 'anyxml' and thus no schema may be present. - ControllerContext: added getRestconfModuleErrorsSchemaNode method to obtain the 'errors' container schema from the ietf-restconf module. - Removed ResponseException and changed occurrences in various classes that threw ResponseException to throw RestconfDocumentedException instead. - Added unit tests for new classes and modified existing ones accordingly. Change-Id: Idbe6f6cae9b40ba14701ee05dfadfdd51e961c6b Signed-off-by: tpantelis --- .../md-sal/sal-rest-connector/pom.xml | 5 + .../controller/sal/rest/api/Draft02.java | 71 +- .../controller/sal/rest/impl/JsonMapper.java | 38 +- .../impl/JsonToCompositeNodeProvider.java | 16 +- .../sal/rest/impl/RestconfApplication.java | 8 + .../RestconfDocumentedExceptionMapper.java | 273 +++++ .../impl/StructuredDataToJsonProvider.java | 6 +- .../impl/StructuredDataToXmlProvider.java | 13 +- .../rest/impl/XmlToCompositeNodeProvider.java | 14 +- .../sal/restconf/impl/BrokerFacade.java | 30 +- .../sal/restconf/impl/ControllerContext.java | 215 +++- .../sal/restconf/impl/ResponseException.java | 27 - .../impl/RestconfDocumentedException.java | 118 +++ .../sal/restconf/impl/RestconfError.java | 221 ++++ .../sal/restconf/impl/RestconfImpl.java | 314 +++--- .../rpc/impl/AbstractRpcExecutor.java | 46 + .../restconf/rpc/impl/BrokerRpcExecutor.java | 2 +- .../rpc/impl/MountPointRpcExecutor.java | 19 +- .../json/to/cnsn/test/JsonToCnSnTest.java | 62 +- .../restconf/impl/test/BrokerFacadeTest.java | 45 +- .../test/CodecsExceptionsCatchingTest.java | 2 + .../impl/test/InvokeRpcMethodTest.java | 178 ++-- .../restconf/impl/test/NormalizeNodeTest.java | 40 +- ...GetAugmentedElementWhenEqualNamesTest.java | 21 +- .../impl/test/RestGetOperationTest.java | 9 +- .../impl/test/RestPostOperationTest.java | 9 +- ...RestconfDocumentedExceptionMapperTest.java | 966 ++++++++++++++++++ .../restconf/impl/test/RestconfErrorTest.java | 236 +++++ .../sal/restconf/impl/test/URITest.java | 36 +- .../toaster/provider/OpendaylightToaster.java | 11 +- 30 files changed, 2513 insertions(+), 538 deletions(-) create mode 100644 opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java delete mode 100644 opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java create mode 100644 opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java create mode 100644 opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java create mode 100644 opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java create mode 100644 opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java diff --git a/opendaylight/md-sal/sal-rest-connector/pom.xml b/opendaylight/md-sal/sal-rest-connector/pom.xml index e4c7c0c647..c2d245badb 100644 --- a/opendaylight/md-sal/sal-rest-connector/pom.xml +++ b/opendaylight/md-sal/sal-rest-connector/pom.xml @@ -88,6 +88,11 @@ mockito-all test + + org.opendaylight.controller + sal-common-util + test + diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/Draft02.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/Draft02.java index d0eaa36dde..af763cce0d 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/Draft02.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/Draft02.java @@ -7,18 +7,67 @@ */ package org.opendaylight.controller.sal.rest.api; +import org.opendaylight.yangtools.yang.common.QName; + public class Draft02 { - public static class MediaTypes { - public static final String API = "application/yang.api"; - public static final String DATASTORE = "application/yang.datastore"; - public static final String DATA = "application/yang.data"; - public static final String OPERATION = "application/yang.operation"; - public static final String PATCH = "application/yang.patch"; - public static final String PATCH_STATUS = "application/yang.patch-status"; - public static final String STREAM = "application/yang.stream"; + public static interface MediaTypes { + String API = "application/yang.api"; + String DATASTORE = "application/yang.datastore"; + String DATA = "application/yang.data"; + String OPERATION = "application/yang.operation"; + String PATCH = "application/yang.patch"; + String PATCH_STATUS = "application/yang.patch-status"; + String STREAM = "application/yang.stream"; + } + + public static interface RestConfModule { + String REVISION = "2013-10-19"; + + String NAME = "ietf-restconf"; + + String NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-restconf"; + + String RESTCONF_GROUPING_SCHEMA_NODE = "restconf"; + + String RESTCONF_CONTAINER_SCHEMA_NODE = "restconf"; + + String MODULES_CONTAINER_SCHEMA_NODE = "modules"; + + String MODULE_LIST_SCHEMA_NODE = "module"; + + String STREAMS_CONTAINER_SCHEMA_NODE = "streams"; + + String STREAM_LIST_SCHEMA_NODE = "stream"; + + String OPERATIONS_CONTAINER_SCHEMA_NODE = "operations"; + + String ERRORS_GROUPING_SCHEMA_NODE = "errors"; + + String ERRORS_CONTAINER_SCHEMA_NODE = "errors"; + + String ERROR_LIST_SCHEMA_NODE = "error"; + + QName IETF_RESTCONF_QNAME = QName.create( Draft02.RestConfModule.NAMESPACE, + Draft02.RestConfModule.REVISION, + Draft02.RestConfModule.NAME ); + + QName ERRORS_CONTAINER_QNAME = QName.create( IETF_RESTCONF_QNAME, ERRORS_CONTAINER_SCHEMA_NODE ); + + QName ERROR_LIST_QNAME = QName.create( IETF_RESTCONF_QNAME, ERROR_LIST_SCHEMA_NODE ); + + QName ERROR_TYPE_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-type" ); + + QName ERROR_TAG_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-tag" ); + + QName ERROR_APP_TAG_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-app-tag" ); + + QName ERROR_MESSAGE_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-message" ); + + QName ERROR_INFO_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-info" ); } - - public static class Paths { - + + + public static interface Paths { + } } diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonMapper.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonMapper.java index ea0f149d29..1e5bfbd6b9 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonMapper.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonMapper.java @@ -11,6 +11,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.net.URI; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -84,17 +85,22 @@ class JsonMapper { private void writeChildrenOfParent(final JsonWriter writer, final CompositeNode parent, final DataNodeContainer parentSchema) throws IOException { checkNotNull(parent); - checkNotNull(parentSchema); + + Set parentSchemaChildNodes = parentSchema == null ? + Collections.emptySet() : parentSchema.getChildNodes(); + for (Node child : parent.getValue()) { - DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchema.getChildNodes()); + DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchemaChildNodes); if (childSchema == null) { - throw new UnsupportedDataTypeException("Probably the data node \"" + child.getNodeType().getLocalName() - + "\" is not conform to schema"); - } + // Node may not conform to schema or allows "anyxml" - we'll process it. - if (childSchema instanceof ContainerSchemaNode) { + logger.debug( "No schema found for data node \"" + child.getNodeType() ); + + handleNoSchemaFound( writer, child, parent ); + } + else if (childSchema instanceof ContainerSchemaNode) { Preconditions.checkState(child instanceof CompositeNode, "Data representation of Container should be CompositeNode - " + child.getNodeType()); writeContainer(writer, (CompositeNode) child, (ContainerSchemaNode) childSchema); @@ -123,7 +129,7 @@ class JsonMapper { } for (Node child : parent.getValue()) { - DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchema.getChildNodes()); + DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchemaChildNodes); if (childSchema instanceof LeafListSchemaNode) { foundLeafLists.remove(childSchema); } else if (childSchema instanceof ListSchemaNode) { @@ -132,6 +138,22 @@ class JsonMapper { } } + private void handleNoSchemaFound( final JsonWriter writer, final Node node, + final CompositeNode parent ) throws IOException { + if( node instanceof SimpleNode ) { + writeName( node, null, writer ); + Object value = node.getValue(); + if( value != null ) { + writer.value( String.valueOf( value ) ); + } + } else { // CompositeNode + Preconditions.checkState( node instanceof CompositeNode, + "Data representation of Container should be CompositeNode - " + node.getNodeType() ); + + writeContainer( writer, (CompositeNode) node, null ); + } + } + private DataSchemaNode findFirstSchemaForNode(final Node node, final Set dataSchemaNode) { for (DataSchemaNode dsn : dataSchemaNode) { if (node.getNodeType().equals(dsn.getQName())) { @@ -301,7 +323,7 @@ class JsonMapper { private void writeName(final Node node, final DataSchemaNode schema, final JsonWriter writer) throws IOException { String nameForOutput = node.getNodeType().getLocalName(); - if (schema.isAugmenting()) { + if ( schema != null && schema.isAugmenting()) { ControllerContext contContext = ControllerContext.getInstance(); CharSequence moduleName = null; if (mountPoint == null) { diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonToCompositeNodeProvider.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonToCompositeNodeProvider.java index 0d73485c80..856e09fabd 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonToCompositeNodeProvider.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonToCompositeNodeProvider.java @@ -16,13 +16,17 @@ import javax.ws.rs.Consumes; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.Provider; import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.rest.api.RestconfService; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; import org.opendaylight.yangtools.yang.data.api.CompositeNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Provider @Consumes({ Draft02.MediaTypes.DATA + RestconfService.JSON, Draft02.MediaTypes.OPERATION + RestconfService.JSON, @@ -30,6 +34,8 @@ import org.opendaylight.yangtools.yang.data.api.CompositeNode; public enum JsonToCompositeNodeProvider implements MessageBodyReader { INSTANCE; + private final static Logger LOG = LoggerFactory.getLogger( JsonToCompositeNodeProvider.class ); + @Override public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return true; @@ -42,9 +48,11 @@ public enum JsonToCompositeNodeProvider implements MessageBodyReader> getClasses() { + return ImmutableSet.>of( RestconfDocumentedExceptionMapper.class ); + } + @Override public Set getSingletons() { Set singletons = new HashSet<>(); @@ -36,4 +43,5 @@ public class RestconfApplication extends Application { return singletons; } + } 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 new file mode 100644 index 0000000000..456354bbf0 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.rest.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +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.core.Response.Status; +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 static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.*; + +import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +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.model.api.DataNodeContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.gson.stream.JsonWriter; + +/** + * 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 ); + + @Context + private HttpHeaders headers; + + @Override + public Response toResponse( RestconfDocumentedException exception ) { + + LOG.debug( "In toResponse: {}", exception.getMessage() ); + + // Default to the content type if there's no Accept header + + MediaType mediaType = headers.getMediaType(); + + List accepts = headers.getAcceptableMediaTypes(); + + LOG.debug( "Accept headers: {}", accepts ); + + if( accepts != null && accepts.size() > 0 ) { + mediaType = accepts.get( 0 ); // just pick the first one + } + + LOG.debug( "Using MediaType: {}", mediaType ); + + 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(); + } + + Status status = errors.iterator().next().getErrorTag().getStatusCode(); + + ControllerContext context = ControllerContext.getInstance(); + DataNodeContainer errorsSchemaNode = (DataNodeContainer)context.getRestconfModuleErrorsSchemaNode(); + + 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 ) ); + } + + ImmutableCompositeNode errorsNode = + ImmutableCompositeNode.create( ERRORS_CONTAINER_QNAME, errorNodes.build() ); + + Object responseBody; + if( mediaType.getSubtype().endsWith( "json" ) ) { + responseBody = toJsonResponseBody( errorsNode, errorsSchemaNode ); + } + else { + responseBody = toXMLResponseBody( errorsNode, errorsSchemaNode ); + } + + return Response.status( status ).type( mediaType ).entity( responseBody ).build(); + } + + private Object toJsonResponseBody( ImmutableCompositeNode errorsNode, + DataNodeContainer errorsSchemaNode ) { + + JsonMapper jsonMapper = new JsonMapper(); + + Object responseBody = null; + try { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + JsonWriter writer = new JsonWriter( new OutputStreamWriter( outStream, "UTF-8" ) ); + writer.setIndent( " " ); + + jsonMapper.write( writer, errorsNode, errorsSchemaNode, null ); + writer.flush(); + + responseBody = outStream.toString( "UTF-8" ); + } + catch( IOException e ) { + LOG.error( "Error writing error response body", e ); + } + + return responseBody; + } + + private Object toXMLResponseBody( ImmutableCompositeNode errorsNode, + DataNodeContainer errorsSchemaNode ) { + + XmlMapper xmlMapper = new XmlMapper(); + + Object responseBody = null; + try { + Document xmlDoc = xmlMapper.write( errorsNode, errorsSchemaNode ); + + responseBody = documentToString( xmlDoc ); + } + catch( TransformerException | UnsupportedDataTypeException | UnsupportedEncodingException e ) { + LOG.error( "Error writing error response body", e ); + } + + return responseBody; + } + + private String documentToString( 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" ); + } + + 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( RestconfError error ) { + + CompositeNodeBuilder builder = ImmutableCompositeNode.builder(); + builder.setQName( ERROR_LIST_QNAME ); + + 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 ); + } + + return builder.toInstance(); + } + + private Node parseErrorInfo( String errorInfo ) { + if( Strings.isNullOrEmpty( errorInfo ) ) { + return null; + } + + 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; + 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, \"" + errorInfo + "\", as XML: " + + e.toString() ); + return null; + } + + 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. + + ImmutableList.Builder> childNodes = ImmutableList.builder(); + for( Entry>> entry: compositeNode.entrySet() ) { + childNodes.addAll( entry.getValue() ); + } + + errorInfoNode = ImmutableCompositeNode.create( ERROR_INFO_QNAME, childNodes.build() ); + } + + return errorInfoNode; + } + + private void addLeaf( CompositeNodeBuilder builder, QName qname, + String value ) { + if( !Strings.isNullOrEmpty( value ) ) { + builder.addLeaf( qname, value ); + } + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/StructuredDataToJsonProvider.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/StructuredDataToJsonProvider.java index 5dba7474ca..422cf04cca 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/StructuredDataToJsonProvider.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/StructuredDataToJsonProvider.java @@ -23,7 +23,7 @@ import javax.ws.rs.ext.Provider; import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.rest.api.RestconfService; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.controller.sal.restconf.impl.StructuredData; import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; @@ -38,7 +38,7 @@ public enum StructuredDataToJsonProvider implements MessageBodyWriter type, Type genericType, Annotation[] annotations, MediaType mediaType) { - return true; + return type.equals( StructuredData.class ); } @Override @@ -52,7 +52,7 @@ public enum StructuredDataToJsonProvider implements MessageBodyWriter type, Type genericType, Annotation[] annotations, MediaType mediaType) { - return true; + return type.equals( StructuredData.class ); } @Override @@ -60,9 +62,9 @@ public enum StructuredDataToXmlProvider implements MessageBodyWriter { INSTANCE; + private final static Logger LOG = LoggerFactory.getLogger( XmlToCompositeNodeProvider.class ); + @Override public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { return true; @@ -45,7 +50,10 @@ public enum XmlToCompositeNodeProvider implements MessageBodyReader invokeRpc( final QName type, final CompositeNode payload ) { + public Future> invokeRpc( final QName type, final CompositeNode payload ) { this.checkPreconditions(); - final Future> future = context.rpc( type, payload ); - - try { - return future.get(); - } - catch( Exception e ) { - throw new ResponseException( e, "Error invoking RPC " + type ); - } + return context.rpc( type, payload ); } public Future> commitConfigurationDataPut( final InstanceIdentifier path, @@ -138,9 +130,9 @@ public class BrokerFacade implements DataReader follow specification - // (http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48) - throw new ResponseException(Status.CONFLICT, errMsg); + + throw new RestconfDocumentedException( + "Data already exists for path: " + path, ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS ); } BrokerFacade.LOG.trace( "Post Configuration via Restconf: {}", path ); transaction.putConfigurationData( path, payload ); @@ -157,9 +149,9 @@ public class BrokerFacade implements DataReader follow specification - // (http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48) - throw new ResponseException(Status.CONFLICT, errMsg); + + throw new RestconfDocumentedException( + "Data already exists for path: " + path, ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS ); } BrokerFacade.LOG.trace( "Post Configuration via Restconf: {}", path ); transaction.putConfigurationData( path, payload ); diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.java index 1c076d1e2e..86ed13a280 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.java @@ -38,11 +38,12 @@ import javax.ws.rs.core.Response.Status; import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.core.api.mount.MountService; +import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.rest.impl.RestUtil; -import org.opendaylight.controller.sal.rest.impl.RestconfProvider; import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; import org.opendaylight.controller.sal.restconf.impl.RestCodec; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; import org.opendaylight.yangtools.concepts.Codec; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier; @@ -55,6 +56,7 @@ import org.opendaylight.yangtools.yang.model.api.ChoiceNode; 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.GroupingDefinition; import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; @@ -108,7 +110,7 @@ public class ControllerContext implements SchemaContextListener { private void checkPreconditions() { if( globalSchema == null ) { - throw new ResponseException( Status.SERVICE_UNAVAILABLE, RestconfProvider.NOT_INITALIZED_MSG ); + throw new RestconfDocumentedException( Status.SERVICE_UNAVAILABLE ); } } @@ -139,8 +141,9 @@ public class ControllerContext implements SchemaContextListener { String first = pathArgs.iterator().next(); final String startModule = ControllerContext.toModuleName( first ); if( startModule == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "First node in URI has to be in format \"moduleName:nodeName\"" ); + throw new RestconfDocumentedException( + "First node in URI has to be in format \"moduleName:nodeName\"", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } InstanceIdentifierBuilder builder = InstanceIdentifier.builder(); @@ -149,7 +152,8 @@ public class ControllerContext implements SchemaContextListener { latestModule, null, toMountPointIdentifier ); if( iiWithSchemaNode == null ) { - throw new ResponseException( Status.BAD_REQUEST, "URI has bad format" ); + throw new RestconfDocumentedException( + "URI has bad format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } return iiWithSchemaNode; @@ -386,6 +390,112 @@ public class ControllerContext implements SchemaContextListener { return builder.toString(); } + public Module getRestconfModule() { + return findModuleByNameAndRevision( Draft02.RestConfModule.IETF_RESTCONF_QNAME ); + } + + public DataSchemaNode getRestconfModuleErrorsSchemaNode() { + Module restconfModule = getRestconfModule(); + if( restconfModule == null ) { + return null; + } + + Set groupings = restconfModule.getGroupings(); + + final Predicate filter = new Predicate() { + @Override + public boolean apply(final GroupingDefinition g) { + return Objects.equal(g.getQName().getLocalName(), + Draft02.RestConfModule.ERRORS_GROUPING_SCHEMA_NODE); + } + }; + + Iterable filteredGroups = Iterables.filter(groupings, filter); + + final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null); + + List instanceDataChildrenByName = + this.findInstanceDataChildrenByName(restconfGrouping, + Draft02.RestConfModule.ERRORS_CONTAINER_SCHEMA_NODE); + return Iterables.getFirst(instanceDataChildrenByName, null); + } + + public DataSchemaNode getRestconfModuleRestConfSchemaNode( Module inRestconfModule, + String schemaNodeName ) { + Module restconfModule = inRestconfModule; + if( restconfModule == null ) { + restconfModule = getRestconfModule(); + } + + if( restconfModule == null ) { + return null; + } + + Set groupings = restconfModule.getGroupings(); + + final Predicate filter = new Predicate() { + @Override + public boolean apply(final GroupingDefinition g) { + return Objects.equal(g.getQName().getLocalName(), + Draft02.RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE); + } + }; + + Iterable filteredGroups = Iterables.filter(groupings, filter); + + final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null); + + List instanceDataChildrenByName = + this.findInstanceDataChildrenByName(restconfGrouping, + Draft02.RestConfModule.RESTCONF_CONTAINER_SCHEMA_NODE); + final DataSchemaNode restconfContainer = Iterables.getFirst(instanceDataChildrenByName, null); + + if (Objects.equal(schemaNodeName, Draft02.RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE)) { + List instances = + this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), + Draft02.RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE); + return Iterables.getFirst(instances, null); + } + else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE)) { + List instances = + this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), + Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + return Iterables.getFirst(instances, null); + } + else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE)) { + List instances = + this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), + Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + final DataSchemaNode modules = Iterables.getFirst(instances, null); + instances = this.findInstanceDataChildrenByName(((DataNodeContainer) modules), + Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE); + return Iterables.getFirst(instances, null); + } + else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE)) { + List instances = + this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), + Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); + return Iterables.getFirst(instances, null); + } + else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE)) { + List instances = + this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), + Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); + final DataSchemaNode modules = Iterables.getFirst(instances, null); + instances = this.findInstanceDataChildrenByName(((DataNodeContainer) modules), + Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE); + return Iterables.getFirst(instances, null); + } + else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE)) { + List instances = + this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), + Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + return Iterables.getFirst(instances, null); + } + + return null; + } + private static DataSchemaNode childByQName( final ChoiceNode container, final QName name ) { for( final ChoiceCaseNode caze : container.getCases() ) { final DataSchemaNode ret = ControllerContext.childByQName( caze, name ); @@ -461,27 +571,30 @@ public class ControllerContext implements SchemaContextListener { if( Objects.equal( moduleName, ControllerContext.MOUNT_MODULE ) && Objects.equal( nodeName, ControllerContext.MOUNT_NODE ) ) { if( mountPoint != null ) { - throw new ResponseException( Status.BAD_REQUEST, - "Restconf supports just one mount point in URI." ); + throw new RestconfDocumentedException( + "Restconf supports just one mount point in URI.", + ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED ); } if( mountService == null ) { - throw new ResponseException( Status.SERVICE_UNAVAILABLE, - "MountService was not found. Finding behind mount points does not work." ); + throw new RestconfDocumentedException( + "MountService was not found. Finding behind mount points does not work.", + ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED ); } final InstanceIdentifier partialPath = builder.toInstance(); final MountInstance mount = mountService.getMountPoint( partialPath ); if( mount == null ) { LOG.debug( "Instance identifier to missing mount point: {}", partialPath ); - throw new ResponseException( Status.BAD_REQUEST, - "Mount point does not exist." ); + throw new RestconfDocumentedException( + "Mount point does not exist.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } final SchemaContext mountPointSchema = mount.getSchemaContext(); if( mountPointSchema == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "Mount point does not contain any schema with modules." ); + throw new RestconfDocumentedException( + "Mount point does not contain any schema with modules.", + ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT ); } if( returnJustMountPoint ) { @@ -496,16 +609,17 @@ public class ControllerContext implements SchemaContextListener { final String moduleNameBehindMountPoint = toModuleName( strings.get( 1 ) ); if( moduleNameBehindMountPoint == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "First node after mount point in URI has to be in format \"moduleName:nodeName\"" ); + throw new RestconfDocumentedException( + "First node after mount point in URI has to be in format \"moduleName:nodeName\"", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final Module moduleBehindMountPoint = this.getLatestModule( mountPointSchema, moduleNameBehindMountPoint ); if( moduleBehindMountPoint == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "URI has bad format. \"" + moduleName + - "\" module does not exist in mount point." ); + throw new RestconfDocumentedException( + "\"" +moduleName + "\" module does not exist in mount point.", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } List subList = strings.subList( 1, strings.size() ); @@ -517,8 +631,9 @@ public class ControllerContext implements SchemaContextListener { if( mountPoint == null ) { module = this.getLatestModule( globalSchema, moduleName ); if( module == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "URI has bad format. \"" + moduleName + "\" module does not exist." ); + throw new RestconfDocumentedException( + "\"" + moduleName + "\" module does not exist.", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } } else { @@ -526,20 +641,20 @@ public class ControllerContext implements SchemaContextListener { module = schemaContext == null ? null : this.getLatestModule( schemaContext, moduleName ); if( module == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "URI has bad format. \"" + moduleName + - "\" module does not exist in mount point." ); + throw new RestconfDocumentedException( + "\"" + moduleName + "\" module does not exist in mount point.", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } } targetNode = this.findInstanceDataChildByNameAndNamespace( parentNode, nodeName, module.getNamespace() );; if( targetNode == null ) { - throw new ResponseException( Status.BAD_REQUEST, - "URI has bad format. Possible reasons:\n" + - "1. \"" + head + "\" was not found in parent data node.\n" + - "2. \"" + head + "\" is behind mount point. Then it should be in format \"/" + - MOUNT + "/" + head + "\"." ); + throw new RestconfDocumentedException( + "URI has bad format. Possible reasons:\n" + + " 1. \"" + head + "\" was not found in parent data node.\n" + + " 2. \"" + head + "\" is behind mount point. Then it should be in format \"/" + + MOUNT + "/" + head + "\".", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } else { final List potentialSchemaNodes = @@ -552,26 +667,27 @@ public class ControllerContext implements SchemaContextListener { .append( "\n" ); } - throw new ResponseException( Status.BAD_REQUEST, + throw new RestconfDocumentedException( "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. " + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"." + "\nThe node is added as augment from modules with namespaces:\n" + - strBuilder.toString() ); + strBuilder.toString(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } if( potentialSchemaNodes.isEmpty() ) { - throw new ResponseException( Status.BAD_REQUEST, "URI has bad format. \"" + nodeName + - "\" was not found in parent data node.\n" ); + throw new RestconfDocumentedException( + "\"" + nodeName + "\" in URI was not found in parent data node", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } targetNode = potentialSchemaNodes.iterator().next(); } if( !this.isListOrContainer( targetNode ) ) { - throw new ResponseException( Status.BAD_REQUEST, - "URI has bad format. Node \"" + head + - "\" must be Container or List yang type." ); + throw new RestconfDocumentedException( + "URI has bad format. Node \"" + head + "\" must be Container or List yang type.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } int consumed = 1; @@ -579,8 +695,9 @@ public class ControllerContext implements SchemaContextListener { final ListSchemaNode listNode = ((ListSchemaNode) targetNode); final int keysSize = listNode.getKeyDefinition().size(); if( (strings.size() - consumed) < keysSize ) { - throw new ResponseException( Status.BAD_REQUEST, "Missing key for list \"" + - listNode.getQName().getLocalName() + "\"." ); + throw new RestconfDocumentedException( + "Missing key for list \"" + listNode.getQName().getLocalName() + "\".", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final List uriKeyValues = strings.subList( consumed, consumed + keysSize ); @@ -590,9 +707,10 @@ public class ControllerContext implements SchemaContextListener { { final String uriKeyValue = uriKeyValues.get( i ); if( uriKeyValue.equals( NULL_VALUE ) ) { - throw new ResponseException( Status.BAD_REQUEST, - "URI has bad format. List \"" + listNode.getQName().getLocalName() + - "\" cannot contain \"null\" value as a key." ); + throw new RestconfDocumentedException( + "URI has bad format. List \"" + listNode.getQName().getLocalName() + + "\" cannot contain \"null\" value as a key.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } this.addKeyValue( keyValues, listNode.getDataChildByName( key ), @@ -711,8 +829,9 @@ public class ControllerContext implements SchemaContextListener { } if( decoded == null ) { - throw new ResponseException( Status.BAD_REQUEST, uriValue + " from URI can\'t be resolved. " + - additionalInfo ); + throw new RestconfDocumentedException( + uriValue + " from URI can't be resolved. " + additionalInfo, + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } map.put( node.getQName(), decoded ); @@ -807,8 +926,9 @@ public class ControllerContext implements SchemaContextListener { return decodedPathArgs; } catch( UnsupportedEncodingException e ) { - throw new ResponseException( Status.BAD_REQUEST, - "Invalid URL path '" + strings + "': " + e.getMessage() ); + throw new RestconfDocumentedException( + "Invalid URL path '" + strings + "': " + e.getMessage(), + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } @@ -818,8 +938,9 @@ public class ControllerContext implements SchemaContextListener { return URLDecoder.decode( pathArg, URI_ENCODING_CHAR_SET ); } catch( UnsupportedEncodingException e ) { - throw new ResponseException( Status.BAD_REQUEST, - "Invalid URL path arg '" + pathArg + "': " + e.getMessage() ); + throw new RestconfDocumentedException( + "Invalid URL path arg '" + pathArg + "': " + e.getMessage(), + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java deleted file mode 100644 index 007fb8eabf..0000000000 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.controller.sal.restconf.impl; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; - -public class ResponseException extends WebApplicationException { - - private static final long serialVersionUID = -5320114450593021655L; - - public ResponseException(Status status, String msg) { - super(Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(msg).build()); - } - - public ResponseException(Throwable cause, String msg) { - super(cause, Response.status(Status.INTERNAL_SERVER_ERROR). - type(MediaType.TEXT_PLAIN_TYPE).entity(msg).build()); - } -} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java new file mode 100644 index 0000000000..0548e95044 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.restconf.impl; + +import java.util.List; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +/** + * Unchecked exception to communicate error information, as defined in the ietf restcong draft, + * to be sent to the client. + * + * @author Devin Avery + * @author Thomas Pantelis + * @see {@link https://tools.ietf.org/html/draft-bierman-netconf-restconf-02} + */ +public class RestconfDocumentedException extends WebApplicationException { + + private static final long serialVersionUID = 1L; + + private final List errors; + private final Status status; + + /** + * Constructs an instance with an error message. The error type defaults to APPLICATION and + * the error tag defaults to OPERATION_FAILED. + * + * @param message A string which provides a plain text string describing the error. + */ + public RestconfDocumentedException( String message ) { + this( message, RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED ); + } + + /** + * Constructs an instance with an error message, error type, and error tag. + * + * @param message A string which provides a plain text string describing the error. + * @param errorType The enumerated type indicating the layer where the error occurred. + * @param errorTag The enumerated tag representing a more specific error cause. + */ + public RestconfDocumentedException( String message, ErrorType errorType, ErrorTag errorTag ) { + this( null, new RestconfError( errorType, errorTag, message ) ); + } + + /** + * Constructs an instance with an error message and exception cause. The stack trace of the + * exception is included in the error info. + * + * @param message A string which provides a plain text string describing the error. + * @param cause The underlying exception cause. + */ + public RestconfDocumentedException( String message, Throwable cause ) { + this( cause, new RestconfError( RestconfError.ErrorType.APPLICATION, + RestconfError.ErrorTag.OPERATION_FAILED, message, + null, RestconfError.toErrorInfo( cause ) ) ); + } + + /** + * Constructs an instance with the given error. + */ + public RestconfDocumentedException( RestconfError error ) { + this( null, error ); + } + + /** + * Constructs an instance with the given errors. + */ + public RestconfDocumentedException( List errors ) { + this.errors = ImmutableList.copyOf( errors ); + Preconditions.checkArgument( !this.errors.isEmpty(), "RestconfError list can't be empty" ); + status = null; + } + + /** + * Constructs an instance with an HTTP status and no error information. + * + * @param status the HTTP status. + */ + public RestconfDocumentedException( Status status ) { + Preconditions.checkNotNull( status, "Status can't be null" ); + errors = ImmutableList.of(); + this.status = status; + } + + private RestconfDocumentedException( Throwable cause, RestconfError error ) { + super( cause ); + Preconditions.checkNotNull( error, "RestconfError can't be null" ); + errors = ImmutableList.of( error ); + status = null; + } + + public List getErrors() { + return errors; + } + + public Status getStatus() { + return status; + } + + + @Override + public String getMessage() { + return "errors: " + errors + (status != null ? ", status: " + status : ""); + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java new file mode 100644 index 0000000000..9220f8bd8f --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java @@ -0,0 +1,221 @@ +/* +* Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved. +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License v1.0 which accompanies this distribution, +* and is available at http://www.eclipse.org/legal/epl-v10.html +*/ +package org.opendaylight.controller.sal.restconf.impl; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.ws.rs.core.Response.Status; + +import org.opendaylight.yangtools.yang.common.RpcError; + +import com.google.common.base.Preconditions; + +/** + * Encapsulates a restconf error as defined in the ietf restconf draft. + * + *

Note: Enumerations defined within are provided by the ietf restconf draft. + * + * @author Devin Avery + * @see {@link https://tools.ietf.org/html/draft-bierman-netconf-restconf-02} + */ +public class RestconfError { + + public static enum ErrorType { + /** Errors relating to the transport layer */ + TRANSPORT, + /** Errors relating to the RPC or notification layer */ + RPC, + /** Errors relating to the protocol operation layer. */ + PROTOCOL, + /** Errors relating to the server application layer. */ + APPLICATION; + + public String getErrorTypeTag() { + return name().toLowerCase(); + } + + public static ErrorType valueOfCaseInsensitive( String value ) + { + try { + return ErrorType.valueOf( ErrorType.class, value.toUpperCase() ); + } + catch( IllegalArgumentException e ) { + return APPLICATION; + } + } + } + + public static enum ErrorTag { + IN_USE( "in-use", Status.fromStatusCode(409)), + INVALID_VALUE( "invalid-value", Status.fromStatusCode(400)), + TOO_BIG( "too-big", Status.fromStatusCode(413)), + MISSING_ATTRIBUTE( "missing-attribute", Status.fromStatusCode(400)), + BAD_ATTRIBUTE( "bad-attribute", Status.fromStatusCode(400)), + UNKNOWN_ATTRIBUTE( "unknown-attribute", Status.fromStatusCode(400)), + BAD_ELEMENT( "bad-element", Status.fromStatusCode(400)), + UNKNOWN_ELEMENT( "unknown-element", Status.fromStatusCode(400)), + UNKNOWN_NAMESPACE( "unknown-namespace", Status.fromStatusCode(400)), + ACCESS_DENIED( "access-denied", Status.fromStatusCode(403)), + LOCK_DENIED( "lock-denied", Status.fromStatusCode(409)), + RESOURCE_DENIED( "resource-denied", Status.fromStatusCode(409)), + ROLLBACK_FAILED( "rollback-failed", Status.fromStatusCode(500)), + DATA_EXISTS( "data-exists", Status.fromStatusCode(409)), + DATA_MISSING( "data-missing", Status.fromStatusCode(409)), + OPERATION_NOT_SUPPORTED( "operation-not-supported", Status.fromStatusCode(501)), + OPERATION_FAILED( "operation-failed", Status.fromStatusCode(500)), + PARTIAL_OPERATION( "partial-operation", Status.fromStatusCode(500)), + MALFORMED_MESSAGE( "malformed-message", Status.fromStatusCode(400)); + + private final String tagValue; + private final Status statusCode; + + ErrorTag(final String tagValue, final Status statusCode) { + this.tagValue = tagValue; + this.statusCode = statusCode; + } + + public String getTagValue() { + return this.tagValue.toLowerCase(); + } + + public static ErrorTag valueOfCaseInsensitive( String value ) + { + try { + return ErrorTag.valueOf( ErrorTag.class, value.toUpperCase().replaceAll( "-","_" ) ); + } + catch( IllegalArgumentException e ) { + return OPERATION_FAILED; + } + } + + public Status getStatusCode() { + return statusCode; + } + } + + private final ErrorType errorType; + private final ErrorTag errorTag; + private final String errorInfo; + private final String errorAppTag; + private final String errorMessage; + //TODO: Add in the error-path concept as defined in the ietf draft. + + static String toErrorInfo( Throwable cause ) { + StringWriter writer = new StringWriter(); + cause.printStackTrace( new PrintWriter( writer ) ); + return writer.toString(); + } + + /** + * Constructs a RestConfError + * + * @param errorType The enumerated type indicating the layer where the error occurred. + * @param errorTag The enumerated tag representing a more specific error cause. + * @param errorMessage A string which provides a plain text string describing the error. + */ + public RestconfError(ErrorType errorType, ErrorTag errorTag, String errorMessage) { + this( errorType, errorTag, errorMessage, null ); + } + + /** + * Constructs a RestConfError object. + * + * @param errorType The enumerated type indicating the layer where the error occurred. + * @param errorTag The enumerated tag representing a more specific error cause. + * @param errorMessage A string which provides a plain text string describing the error. + * @param errorAppTag A string which represents an application-specific error tag that further + * specifies the error cause. + */ + public RestconfError(ErrorType errorType, ErrorTag errorTag, String errorMessage, + String errorAppTag) { + this( errorType, errorTag, errorMessage, errorAppTag, null ); + } + + /** + * Constructs a RestConfError object. + * + * @param errorType The enumerated type indicating the layer where the error occurred. + * @param errorTag The enumerated tag representing a more specific error cause. + * @param errorMessage A string which provides a plain text string describing the error. + * @param errorAppTag A string which represents an application-specific error tag that further + * specifies the error cause. + * @param errorInfo A string, formatted as XML, which contains additional error information. + */ + public RestconfError(ErrorType errorType, ErrorTag errorTag, String errorMessage, + String errorAppTag, String errorInfo) { + Preconditions.checkNotNull( errorType, "Error type is required for RestConfError" ); + Preconditions.checkNotNull( errorTag, "Error tag is required for RestConfError"); + this.errorType = errorType; + this.errorTag = errorTag; + this.errorMessage = errorMessage; + this.errorAppTag = errorAppTag; + this.errorInfo = errorInfo; + } + + /** + * Constructs a RestConfError object from an RpcError. + */ + public RestconfError( RpcError rpcError ) { + + this.errorType = rpcError.getErrorType() == null ? ErrorType.APPLICATION : + ErrorType.valueOfCaseInsensitive( rpcError.getErrorType().name() ); + + this.errorTag = rpcError.getTag() == null ? ErrorTag.OPERATION_FAILED : + ErrorTag.valueOfCaseInsensitive( rpcError.getTag().toString() ); + + this.errorMessage = rpcError.getMessage(); + this.errorAppTag = rpcError.getApplicationTag(); + + String errorInfo = null; + if( rpcError.getInfo() == null ) { + if( rpcError.getCause() != null ) { + errorInfo = toErrorInfo( rpcError.getCause() ); + } + else if( rpcError.getSeverity() != null ) { + errorInfo = "" + rpcError.getSeverity().toString().toLowerCase() + + ""; + } + } + else { + errorInfo = rpcError.getInfo(); + } + + this.errorInfo = errorInfo; + } + + public ErrorType getErrorType() { + return errorType; + } + + public ErrorTag getErrorTag() { + return errorTag; + } + + public String getErrorInfo() { + return errorInfo; + } + + public String getErrorAppTag() { + return errorAppTag; + } + + public String getErrorMessage() { + return errorMessage; + } + + @Override + public String toString() { + return "error-type: " + errorType.getErrorTypeTag() + + ", error-tag: " + errorTag.getTagValue() + ", " + + (errorAppTag != null ? "error-app-tag: " + errorAppTag + ", " : "") + + (errorMessage != null ? "error-message: " + errorMessage : "") + + (errorInfo != null ? "error-info: " + errorInfo + ", " : "") + "]"; + } + +} \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java index e9d489dd35..ad682bc829 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java @@ -8,11 +8,19 @@ */ package org.opendaylight.controller.sal.restconf.impl; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + import java.net.URI; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -28,15 +36,29 @@ import javax.ws.rs.core.UriInfo; import org.apache.commons.lang3.StringUtils; import org.opendaylight.controller.md.sal.common.api.TransactionStatus; import org.opendaylight.controller.sal.core.api.mount.MountInstance; +import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.rest.api.RestconfService; import org.opendaylight.controller.sal.restconf.rpc.impl.BrokerRpcExecutor; import org.opendaylight.controller.sal.restconf.rpc.impl.MountPointRpcExecutor; import org.opendaylight.controller.sal.restconf.rpc.impl.RpcExecutor; +import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; +import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper; +import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +import org.opendaylight.controller.sal.restconf.impl.EmptyNodeWrapper; +import org.opendaylight.controller.sal.restconf.impl.IdentityValuesDTO; +import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode; +import org.opendaylight.controller.sal.restconf.impl.NodeWrapper; +import org.opendaylight.controller.sal.restconf.impl.RestCodec; +import org.opendaylight.controller.sal.restconf.impl.SimpleNodeWrapper; +import org.opendaylight.controller.sal.restconf.impl.StructuredData; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; import org.opendaylight.controller.sal.streams.listeners.ListenerAdapter; import org.opendaylight.controller.sal.streams.listeners.Notificator; import org.opendaylight.controller.sal.streams.websockets.WebSocketServer; import org.opendaylight.yangtools.concepts.Codec; import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.RpcError; import org.opendaylight.yangtools.yang.common.RpcResult; import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier; @@ -49,7 +71,6 @@ 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.FeatureDefinition; -import org.opendaylight.yangtools.yang.model.api.GroupingDefinition; import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; @@ -63,15 +84,6 @@ import org.opendaylight.yangtools.yang.model.util.EmptyType; import org.opendaylight.yangtools.yang.parser.builder.impl.ContainerSchemaNodeBuilder; import org.opendaylight.yangtools.yang.parser.builder.impl.LeafSchemaNodeBuilder; -import com.google.common.base.Objects; -import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; -import com.google.common.base.Splitter; -import com.google.common.base.Strings; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; - -@SuppressWarnings("all") public class RestconfImpl implements RestconfService { private final static RestconfImpl INSTANCE = new RestconfImpl(); @@ -81,26 +93,6 @@ public class RestconfImpl implements RestconfService { private final static SimpleDateFormat REVISION_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); - private final static String RESTCONF_MODULE_DRAFT02_REVISION = "2013-10-19"; - - private final static String RESTCONF_MODULE_DRAFT02_NAME = "ietf-restconf"; - - private final static String RESTCONF_MODULE_DRAFT02_NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-restconf"; - - private final static String RESTCONF_MODULE_DRAFT02_RESTCONF_GROUPING_SCHEMA_NODE = "restconf"; - - private final static String RESTCONF_MODULE_DRAFT02_RESTCONF_CONTAINER_SCHEMA_NODE = "restconf"; - - private final static String RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE = "modules"; - - private final static String RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE = "module"; - - private final static String RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE = "streams"; - - private final static String RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE = "stream"; - - private final static String RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE = "operations"; - private final static String SAL_REMOTE_NAMESPACE = "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"; private final static String SAL_REMOTE_RPC_SUBSRCIBE = "create-data-change-event-subscription"; @@ -129,8 +121,8 @@ public class RestconfImpl implements RestconfService { final Module restconfModule = this.getRestconfModule(); final List> modulesAsData = new ArrayList>(); - final DataSchemaNode moduleSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE); + final DataSchemaNode moduleSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE); Set allModules = this.controllerContext.getAllModules(); for (final Module module : allModules) { @@ -138,8 +130,8 @@ public class RestconfImpl implements RestconfService { modulesAsData.add(moduleCompositeNode); } - final DataSchemaNode modulesSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE); + final DataSchemaNode modulesSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); QName qName = modulesSchemaNode.getQName(); final CompositeNode modulesNode = NodeFactory.createImmutableCompositeNode(qName, null, modulesAsData); return new StructuredData(modulesNode, modulesSchemaNode, null); @@ -151,14 +143,14 @@ public class RestconfImpl implements RestconfService { final List> streamsAsData = new ArrayList>(); Module restconfModule = this.getRestconfModule(); - final DataSchemaNode streamSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE); + final DataSchemaNode streamSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE); for (final String streamName : availableStreams) { streamsAsData.add(this.toStreamCompositeNode(streamName, streamSchemaNode)); } - final DataSchemaNode streamsSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE); + final DataSchemaNode streamsSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); QName qName = streamsSchemaNode.getQName(); final CompositeNode streamsNode = NodeFactory.createImmutableCompositeNode(qName, null, streamsAsData); return new StructuredData(streamsNode, streamsSchemaNode, null); @@ -175,22 +167,22 @@ public class RestconfImpl implements RestconfService { modules = this.controllerContext.getAllModules(mountPoint); } else { - throw new ResponseException(Status.BAD_REQUEST, - "URI has bad format. If modules behind mount point should be showed, URI has to end with " + - ControllerContext.MOUNT); + throw new RestconfDocumentedException( + "URI has bad format. If modules behind mount point should be showed, URI has to end with " + + ControllerContext.MOUNT, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final List> modulesAsData = new ArrayList>(); Module restconfModule = this.getRestconfModule(); - final DataSchemaNode moduleSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE); + final DataSchemaNode moduleSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE); for (final Module module : modules) { modulesAsData.add(this.toModuleCompositeNode(module, moduleSchemaNode)); } - final DataSchemaNode modulesSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE); + final DataSchemaNode modulesSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); QName qName = modulesSchemaNode.getQName(); final CompositeNode modulesNode = NodeFactory.createImmutableCompositeNode(qName, null, modulesAsData); return new StructuredData(modulesNode, modulesSchemaNode, mountPoint); @@ -212,14 +204,15 @@ public class RestconfImpl implements RestconfService { } if (module == null) { - throw new ResponseException(Status.BAD_REQUEST, + throw new RestconfDocumentedException( "Module with name '" + moduleNameAndRevision.getLocalName() + "' and revision '" + - moduleNameAndRevision.getRevision() + "' was not found."); + moduleNameAndRevision.getRevision() + "' was not found.", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } Module restconfModule = this.getRestconfModule(); - final DataSchemaNode moduleSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE); + final DataSchemaNode moduleSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE); final CompositeNode moduleNode = this.toModuleCompositeNode(module, moduleSchemaNode); return new StructuredData(moduleNode, moduleSchemaNode, mountPoint); } @@ -241,9 +234,9 @@ public class RestconfImpl implements RestconfService { modules = this.controllerContext.getAllModules(mountPoint); } else { - throw new ResponseException(Status.BAD_REQUEST, - "URI has bad format. If operations behind mount point should be showed, URI has to end with " + - ControllerContext.MOUNT); + throw new RestconfDocumentedException( + "URI has bad format. If operations behind mount point should be showed, URI has to end with " + + ControllerContext.MOUNT, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } return this.operationsFromModulesToStructuredData(modules, mountPoint); @@ -253,12 +246,12 @@ public class RestconfImpl implements RestconfService { final MountInstance mountPoint) { final List> operationsAsData = new ArrayList>(); Module restconfModule = this.getRestconfModule(); - final DataSchemaNode operationsSchemaNode = - this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE); + final DataSchemaNode operationsSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode( + restconfModule, Draft02.RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE); QName qName = operationsSchemaNode.getQName(); SchemaPath path = operationsSchemaNode.getPath(); ContainerSchemaNodeBuilder containerSchemaNodeBuilder = - new ContainerSchemaNodeBuilder(RESTCONF_MODULE_DRAFT02_NAME, 0, qName, path); + new ContainerSchemaNodeBuilder(Draft02.RestConfModule.NAME, 0, qName, path); final ContainerSchemaNodeBuilder fakeOperationsSchemaNode = containerSchemaNodeBuilder; for (final Module module : modules) { Set rpcs = module.getRpcs(); @@ -286,11 +279,11 @@ public class RestconfImpl implements RestconfService { } private Module getRestconfModule() { - QName qName = QName.create(RESTCONF_MODULE_DRAFT02_NAMESPACE, RESTCONF_MODULE_DRAFT02_REVISION, - RESTCONF_MODULE_DRAFT02_NAME); - final Module restconfModule = this.controllerContext.findModuleByNameAndRevision(qName); + Module restconfModule = controllerContext.getRestconfModule(); if (restconfModule == null) { - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, "Restconf module was not found."); + throw new RestconfDocumentedException( + "ietf-restconf module was not found.", ErrorType.APPLICATION, + ErrorTag.OPERATION_NOT_SUPPORTED ); } return restconfModule; @@ -310,8 +303,9 @@ public class RestconfImpl implements RestconfService { Iterable split = splitter.split(moduleNameAndRevision); final List pathArgs = Lists.newArrayList(split); if (pathArgs.size() < 2) { - throw new ResponseException(Status.BAD_REQUEST, - "URI has bad format. End of URI should be in format \'moduleName/yyyy-MM-dd\'"); + throw new RestconfDocumentedException( + "URI has bad format. End of URI should be in format \'moduleName/yyyy-MM-dd\'", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } try { @@ -321,7 +315,9 @@ public class RestconfImpl implements RestconfService { return QName.create(null, moduleRevision, moduleName); } catch (ParseException e) { - throw new ResponseException(Status.BAD_REQUEST, "URI has bad format. It should be \'moduleName/yyyy-MM-dd\'"); + throw new RestconfDocumentedException( + "URI has bad format. It should be \'moduleName/yyyy-MM-dd\'", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } @@ -393,66 +389,6 @@ public class RestconfImpl implements RestconfService { return NodeFactory.createImmutableCompositeNode(moduleSchemaNode.getQName(), null, moduleNodeValues); } - private DataSchemaNode getSchemaNode(final Module restconfModule, final String schemaNodeName) { - Set groupings = restconfModule.getGroupings(); - - final Predicate filter = new Predicate() { - @Override - public boolean apply(final GroupingDefinition g) { - return Objects.equal(g.getQName().getLocalName(), - RESTCONF_MODULE_DRAFT02_RESTCONF_GROUPING_SCHEMA_NODE); - } - }; - - Iterable filteredGroups = Iterables.filter(groupings, filter); - - final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null); - - List instanceDataChildrenByName = - this.controllerContext.findInstanceDataChildrenByName(restconfGrouping, - RESTCONF_MODULE_DRAFT02_RESTCONF_CONTAINER_SCHEMA_NODE); - final DataSchemaNode restconfContainer = Iterables.getFirst(instanceDataChildrenByName, null); - - if (Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE)) { - List instances = - this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), - RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE); - return Iterables.getFirst(instances, null); - } - else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE)) { - List instances = - this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), - RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE); - return Iterables.getFirst(instances, null); - } - else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE)) { - List instances = - this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), - RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE); - final DataSchemaNode modules = Iterables.getFirst(instances, null); - instances = this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) modules), - RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE); - return Iterables.getFirst(instances, null); - } - else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE)) { - List instances = - this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), - RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE); - return Iterables.getFirst(instances, null); - } - else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE)) { - List instances = - this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer), - RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE); - final DataSchemaNode modules = Iterables.getFirst(instances, null); - instances = this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) modules), - RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE); - return Iterables.getFirst(instances, null); - } - - return null; - } - @Override public Object getRoot() { return null; @@ -480,8 +416,9 @@ public class RestconfImpl implements RestconfService { final Object pathValue = pathNode == null ? null : pathNode.getValue(); if (!(pathValue instanceof InstanceIdentifier)) { - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, - "Instance identifier was not normalized correctly."); + throw new RestconfDocumentedException( + "Instance identifier was not normalized correctly.", + ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED ); } final InstanceIdentifier pathIdentifier = ((InstanceIdentifier) pathValue); @@ -492,8 +429,9 @@ public class RestconfImpl implements RestconfService { } if (Strings.isNullOrEmpty(streamName)) { - throw new ResponseException(Status.BAD_REQUEST, - "Path is empty or contains data node which is not Container or List build-in type."); + throw new RestconfDocumentedException( + "Path is empty or contains data node which is not Container or List build-in type.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final SimpleNode streamNameNode = NodeFactory.createImmutableSimpleNode( @@ -514,8 +452,8 @@ public class RestconfImpl implements RestconfService { @Override public StructuredData invokeRpc(final String identifier, final String noPayload) { if (StringUtils.isNotBlank(noPayload)) { - throw new ResponseException( - Status.UNSUPPORTED_MEDIA_TYPE, "Content-Type contains unsupported Media Type."); + throw new RestconfDocumentedException( + "Content must be empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final RpcExecutor rpc = resolveIdentifierInInvokeRpc(identifier); return callRpc(rpc, null); @@ -540,7 +478,8 @@ public class RestconfImpl implements RestconfService { .format("Identifier %n%s%ncan\'t contain slash " + "character (/).%nIf slash is part of identifier name then use %%2F placeholder.", identifier); - throw new ResponseException(Status.NOT_FOUND, slashErrorMsg); + throw new RestconfDocumentedException( + slashErrorMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } else { identifierEncoded = identifier; } @@ -549,7 +488,8 @@ public class RestconfImpl implements RestconfService { RpcDefinition rpc = controllerContext.getRpcDefinition(identifierDecoded); if (rpc == null) { - throw new ResponseException(Status.NOT_FOUND, "RPC does not exist."); + throw new RestconfDocumentedException( + "RPC does not exist.", ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT ); } if (mountPoint == null) { @@ -562,7 +502,8 @@ public class RestconfImpl implements RestconfService { private StructuredData callRpc(final RpcExecutor rpcExecutor, final CompositeNode payload) { if (rpcExecutor == null) { - throw new ResponseException(Status.NOT_FOUND, "RPC does not exist."); + throw new RestconfDocumentedException( + "RPC does not exist.", ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT ); } CompositeNode rpcRequest = null; @@ -595,9 +536,20 @@ public class RestconfImpl implements RestconfService { private void checkRpcSuccessAndThrowException(RpcResult rpcResult) { if (rpcResult.isSuccessful() == false) { - //TODO: Get smart about what error code we are return (Future Bug coming) - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, - "The operation was not successful and there were no RPC errors returned"); + + Collection rpcErrors = rpcResult.getErrors(); + if( rpcErrors == null || rpcErrors.isEmpty() ) { + throw new RestconfDocumentedException( + "The operation was not successful and there were no RPC errors returned", + ErrorType.RPC, ErrorTag.OPERATION_FAILED ); + } + + List errorList = Lists.newArrayList(); + for( RpcError rpcError: rpcErrors ) { + errorList.add( new RestconfError( rpcError ) ); + } + + throw new RestconfDocumentedException( errorList ); } } @@ -647,7 +599,7 @@ public class RestconfImpl implements RestconfService { } } catch( Exception e ) { - throw new ResponseException( e, "Error updating data" ); + throw new RestconfDocumentedException( "Error updating data", e ); } if( status.getResult() == TransactionStatus.COMMITED ) @@ -660,8 +612,9 @@ public class RestconfImpl implements RestconfService { public Response createConfigurationData(final String identifier, final CompositeNode payload) { URI payloadNS = this.namespace(payload); if (payloadNS == null) { - throw new ResponseException(Status.BAD_REQUEST, - "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)"); + throw new RestconfDocumentedException( + "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_NAMESPACE ); } InstanceIdWithSchemaNode iiWithData = null; @@ -670,9 +623,10 @@ public class RestconfImpl implements RestconfService { // payload represents mount point data and URI represents path to the mount point if (this.endsWithMountPoint(identifier)) { - throw new ResponseException(Status.BAD_REQUEST, - "URI has bad format. URI should be without \"" + ControllerContext.MOUNT + - "\" for POST operation."); + throw new RestconfDocumentedException( + "URI has bad format. URI should be without \"" + ControllerContext.MOUNT + + "\" for POST operation.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final String completeIdentifier = this.addMountPointIdentifier(identifier); @@ -687,8 +641,9 @@ public class RestconfImpl implements RestconfService { MountInstance mountPoint = incompleteInstIdWithData.getMountPoint(); final Module module = this.findModule(mountPoint, payload); if (module == null) { - throw new ResponseException(Status.BAD_REQUEST, - "Module was not found for \"" + payloadNS + "\""); + throw new RestconfDocumentedException( + "Module was not found for \"" + payloadNS + "\"", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } String payloadName = this.getName(payload); @@ -714,9 +669,8 @@ public class RestconfImpl implements RestconfService { status = future == null ? null : future.get(); } } - catch( ResponseException e ){ throw e; } catch( Exception e ) { - throw new ResponseException( e, "Error creating data" ); + throw new RestconfDocumentedException( "Error creating data", e ); } if (status == null) { @@ -733,14 +687,16 @@ public class RestconfImpl implements RestconfService { public Response createConfigurationData(final CompositeNode payload) { URI payloadNS = this.namespace(payload); if (payloadNS == null) { - throw new ResponseException(Status.BAD_REQUEST, - "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)"); + throw new RestconfDocumentedException( + "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_NAMESPACE ); } final Module module = this.findModule(null, payload); if (module == null) { - throw new ResponseException(Status.BAD_REQUEST, - "Data has bad format. Root element node has incorrect namespace (XML format) or module name(JSON format)"); + throw new RestconfDocumentedException( + "Data has bad format. Root element node has incorrect namespace (XML format) or module name(JSON format)", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_NAMESPACE ); } String payloadName = this.getName(payload); @@ -764,9 +720,8 @@ public class RestconfImpl implements RestconfService { status = future == null ? null : future.get(); } } - catch( ResponseException e ){ throw e; } catch( Exception e ) { - throw new ResponseException( e, "Error creating data" ); + throw new RestconfDocumentedException( "Error creating data", e ); } if (status == null) { @@ -795,7 +750,7 @@ public class RestconfImpl implements RestconfService { } } catch( Exception e ) { - throw new ResponseException( e, "Error creating data" ); + throw new RestconfDocumentedException( "Error creating data", e ); } if( status.getResult() == TransactionStatus.COMMITED ) @@ -808,12 +763,14 @@ public class RestconfImpl implements RestconfService { public Response subscribeToStream(final String identifier, final UriInfo uriInfo) { final String streamName = Notificator.createStreamNameFromUri(identifier); if (Strings.isNullOrEmpty(streamName)) { - throw new ResponseException(Status.BAD_REQUEST, "Stream name is empty."); + throw new RestconfDocumentedException( + "Stream name is empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } final ListenerAdapter listener = Notificator.getListenerFor(streamName); if (listener == null) { - throw new ResponseException(Status.BAD_REQUEST, "Stream was not found."); + throw new RestconfDocumentedException( + "Stream was not found.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT ); } broker.registerToListenDataChanges(listener); @@ -917,9 +874,10 @@ public class RestconfImpl implements RestconfService { } if (dataNodeKeyValueObject == null) { - throw new ResponseException(Status.BAD_REQUEST, - "Data contains list \"" + dataNode.getNodeType().getLocalName() + - "\" which does not contain key: \"" + key.getLocalName() + "\""); + throw new RestconfDocumentedException( + "Data contains list \"" + dataNode.getNodeType().getLocalName() + + "\" which does not contain key: \"" + key.getLocalName() + "\"", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } keyValues.put(key, dataNodeKeyValueObject); @@ -954,14 +912,16 @@ public class RestconfImpl implements RestconfService { if (schema == null) { QName nodeType = node == null ? null : node.getNodeType(); String localName = nodeType == null ? null : nodeType.getLocalName(); - String _plus = ("Data schema node was not found for " + localName); - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, - "Data schema node was not found for " + localName ); + + throw new RestconfDocumentedException( + "Data schema node was not found for " + localName, + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } if (!(schema instanceof DataNodeContainer)) { - throw new ResponseException(Status.BAD_REQUEST, - "Root element has to be container or list yang datatype."); + throw new RestconfDocumentedException( + "Root element has to be container or list yang datatype.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } if ((node instanceof CompositeNodeWrapper)) { @@ -970,8 +930,9 @@ public class RestconfImpl implements RestconfService { try { this.normalizeNode(((CompositeNodeWrapper) node), schema, null, mountPoint); } - catch (NumberFormatException e) { - throw new ResponseException(Status.BAD_REQUEST, e.getMessage()); + catch (IllegalArgumentException e) { + throw new RestconfDocumentedException( + e.getMessage(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } @@ -985,9 +946,10 @@ public class RestconfImpl implements RestconfService { final DataSchemaNode schema, final QName previousAugment, final MountInstance mountPoint) { if (schema == null) { - throw new ResponseException(Status.BAD_REQUEST, - "Data has bad format.\n\"" + nodeBuilder.getLocalName() + - "\" does not exist in yang schema."); + throw new RestconfDocumentedException( + "Data has bad format.\n\"" + nodeBuilder.getLocalName() + + "\" does not exist in yang schema.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } QName currentAugment = null; @@ -997,13 +959,14 @@ public class RestconfImpl implements RestconfService { else { currentAugment = this.normalizeNodeName(nodeBuilder, schema, previousAugment, mountPoint); if (nodeBuilder.getQname() == null) { - throw new ResponseException(Status.BAD_REQUEST, + throw new RestconfDocumentedException( "Data has bad format.\nIf data is in XML format then namespace for \"" + nodeBuilder.getLocalName() + "\" should be \"" + schema.getQName().getNamespace() + "\".\n" + "If data is in JSON format then module name for \"" + nodeBuilder.getLocalName() + "\" should be corresponding to namespace \"" + - schema.getQName().getNamespace() + "\"."); + schema.getQName().getNamespace() + "\".", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } @@ -1021,11 +984,12 @@ public class RestconfImpl implements RestconfService { .append("\n"); } - throw new ResponseException(Status.BAD_REQUEST, + throw new RestconfDocumentedException( "Node \"" + child.getLocalName() + "\" is added as augment from more than one module. " + "Therefore node must have namespace (XML format) or module name (JSON format)." + - "\nThe node is added as augment from modules with namespaces:\n" + builder); + "\nThe node is added as augment from modules with namespaces:\n" + builder, + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } boolean rightNodeSchemaFound = false; @@ -1041,8 +1005,9 @@ public class RestconfImpl implements RestconfService { } if (!rightNodeSchemaFound) { - throw new ResponseException(Status.BAD_REQUEST, - "Schema node \"" + child.getLocalName() + "\" was not found in module."); + throw new RestconfDocumentedException( + "Schema node \"" + child.getLocalName() + "\" was not found in module.", + ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT ); } } @@ -1057,9 +1022,10 @@ public class RestconfImpl implements RestconfService { } if (!foundKey) { - throw new ResponseException(Status.BAD_REQUEST, + throw new RestconfDocumentedException( "Missing key in URI \"" + listKey.getLocalName() + - "\" of list \"" + schema.getQName().getLocalName() + "\""); + "\" of list \"" + schema.getQName().getLocalName() + "\"", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); } } } diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java index 446d07d426..0bc8428d76 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java @@ -7,6 +7,15 @@ */ package org.opendaylight.controller.sal.restconf.rpc.impl; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; +import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; public abstract class AbstractRpcExecutor implements RpcExecutor { @@ -20,4 +29,41 @@ public abstract class AbstractRpcExecutor implements RpcExecutor { public RpcDefinition getRpcDefinition() { return rpcDef; } + + protected RpcResult getRpcResult( + Future> fromFuture ) { + try { + return fromFuture.get(); + } + catch( InterruptedException e ) { + throw new RestconfDocumentedException( + "The operation was interrupted while executing and did not complete.", + ErrorType.RPC, ErrorTag.PARTIAL_OPERATION ); + } + catch( ExecutionException e ) { + Throwable cause = e.getCause(); + if( cause instanceof CancellationException ) { + throw new RestconfDocumentedException( + "The operation was cancelled while executing.", + ErrorType.RPC, ErrorTag.PARTIAL_OPERATION ); + } + else if( cause != null ){ + while( cause.getCause() != null ) { + cause = cause.getCause(); + } + + if( cause instanceof IllegalArgumentException ) { + throw new RestconfDocumentedException( + cause.getMessage(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE ); + } + + throw new RestconfDocumentedException( + "The operation encountered an unexpected error while executing.", cause ); + } + else { + throw new RestconfDocumentedException( + "The operation encountered an unexpected error while executing.", e ); + } + } + } } \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java index 0748832247..249b657d49 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java @@ -23,6 +23,6 @@ public class BrokerRpcExecutor extends AbstractRpcExecutor { @Override public RpcResult invokeRpc(CompositeNode rpcRequest) { - return broker.invokeRpc( getRpcDefinition().getQName(), rpcRequest ); + return getRpcResult( broker.invokeRpc( getRpcDefinition().getQName(), rpcRequest ) ); } } \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java index b56db21951..da19a0034d 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java @@ -7,18 +7,13 @@ */ package org.opendaylight.controller.sal.restconf.rpc.impl; -import java.util.concurrent.ExecutionException; - -import javax.ws.rs.core.Response.Status; - import org.opendaylight.controller.sal.core.api.mount.MountInstance; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.yangtools.yang.common.RpcResult; import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; import com.google.common.base.Preconditions; -import com.google.common.util.concurrent.ListenableFuture; /** * Provides an implementation which invokes rpc methods via a mounted yang data model. @@ -35,14 +30,8 @@ public class MountPointRpcExecutor extends AbstractRpcExecutor { } @Override - public RpcResult invokeRpc( CompositeNode rpcRequest ) throws ResponseException { - ListenableFuture> rpcFuture = - mountPoint.rpc( getRpcDefinition().getQName(), rpcRequest); - try { - return rpcFuture.get(); - } catch (InterruptedException | ExecutionException e) { - throw new ResponseException(Status.INTERNAL_SERVER_ERROR, - e.getCause().getMessage() ); - } + public RpcResult invokeRpc( CompositeNode rpcRequest ) + throws RestconfDocumentedException { + return getRpcResult( mountPoint.rpc( getRpcDefinition().getQName(), rpcRequest ) ); } } \ No newline at end of file diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/json/to/cnsn/test/JsonToCnSnTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/json/to/cnsn/test/JsonToCnSnTest.java index 415d58e53d..3c70cca0f8 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/json/to/cnsn/test/JsonToCnSnTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/json/to/cnsn/test/JsonToCnSnTest.java @@ -8,7 +8,6 @@ package org.opendaylight.controller.sal.restconf.impl.json.to.cnsn.test; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -16,13 +15,11 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import javax.ws.rs.WebApplicationException; - import org.junit.Ignore; import org.junit.Test; import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider; import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.controller.sal.restconf.impl.test.TestUtils; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.CompositeNode; @@ -32,8 +29,6 @@ import org.opendaylight.yangtools.yang.model.api.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonSyntaxException; - public class JsonToCnSnTest { private static final Logger LOG = LoggerFactory.getLogger(JsonToCnSnTest.class); @@ -116,44 +111,39 @@ public class JsonToCnSnTest { @Test public void incorrectTopLevelElementsTest() { - Throwable cause1 = null; + RestconfDocumentedException cause1 = null; try { - TestUtils - .readInputToCnSn("/json-to-cnsn/wrong-top-level1.json", true, JsonToCompositeNodeProvider.INSTANCE); - } catch (WebApplicationException e) { + TestUtils.readInputToCnSn("/json-to-cnsn/wrong-top-level1.json", true, JsonToCompositeNodeProvider.INSTANCE); + } catch (RestconfDocumentedException e) { cause1 = e; } assertNotNull(cause1); - assertTrue(cause1 - .getCause() - .getMessage() - .contains( - "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet.")); + assertTrue(cause1.getErrors().get( 0 ).getErrorMessage().contains( + "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet.")); - Throwable cause2 = null; + RestconfDocumentedException cause2 = null; try { TestUtils - .readInputToCnSn("/json-to-cnsn/wrong-top-level2.json", true, JsonToCompositeNodeProvider.INSTANCE); - } catch (WebApplicationException e) { + .readInputToCnSn("/json-to-cnsn/wrong-top-level2.json", true, JsonToCompositeNodeProvider.INSTANCE); + } catch (RestconfDocumentedException e) { cause2 = e; } assertNotNull(cause2); - assertTrue(cause2.getCause().getMessage().contains("Json Object should contain one element")); + assertTrue(cause2.getErrors().get( 0 ).getErrorMessage().contains( + "Json Object should contain one element")); - Throwable cause3 = null; + RestconfDocumentedException cause3 = null; try { TestUtils - .readInputToCnSn("/json-to-cnsn/wrong-top-level3.json", true, JsonToCompositeNodeProvider.INSTANCE); - } catch (WebApplicationException e) { + + .readInputToCnSn("/json-to-cnsn/wrong-top-level3.json", true, JsonToCompositeNodeProvider.INSTANCE); + } catch (RestconfDocumentedException e) { cause3 = e; } assertNotNull(cause3); - assertTrue(cause3 - .getCause() - .getMessage() - .contains( - "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet.")); + assertTrue(cause3.getErrors().get( 0 ).getErrorMessage().contains( + "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet.")); } @@ -178,8 +168,8 @@ public class JsonToCnSnTest { String reason = null; try { TestUtils.readInputToCnSn("/json-to-cnsn/empty-data1.json", true, JsonToCompositeNodeProvider.INSTANCE); - } catch (JsonSyntaxException e) { - reason = e.getMessage(); + } catch (RestconfDocumentedException e) { + reason = e.getErrors().get( 0 ).getErrorMessage(); } assertTrue(reason.contains("Expected value at line")); @@ -278,15 +268,9 @@ public class JsonToCnSnTest { @Ignore @Test public void loadDataAugmentedSchemaMoreEqualNamesTest() { - boolean exceptionCaught = false; - try { - loadAndNormalizeData("/common/augment/json/dataa.json", "/common/augment/yang", "cont", "main"); - loadAndNormalizeData("/common/augment/json/datab.json", "/common/augment/yang", "cont", "main"); - } catch (ResponseException e) { - exceptionCaught = true; - } + loadAndNormalizeData("/common/augment/json/dataa.json", "/common/augment/yang", "cont", "main"); + loadAndNormalizeData("/common/augment/json/datab.json", "/common/augment/yang", "cont", "main"); - assertFalse(exceptionCaught); } private void simpleTest(final String jsonPath, final String yangPath, final String topLevelElementName, final String namespace, @@ -399,8 +383,8 @@ public class JsonToCnSnTest { try { TestUtils.readInputToCnSn("/json-to-cnsn/unsupported-json-format.json", true, JsonToCompositeNodeProvider.INSTANCE); - } catch (WebApplicationException e) { - exceptionMessage = e.getCause().getMessage(); + } catch (RestconfDocumentedException e) { + exceptionMessage = e.getErrors().get( 0 ).getErrorMessage(); } assertTrue(exceptionMessage.contains("Root element of Json has to be Object")); } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java index 18199de8c6..ddab700440 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java @@ -9,6 +9,7 @@ package org.opendaylight.controller.sal.restconf.impl.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -19,8 +20,6 @@ import static org.mockito.Mockito.when; import java.util.Map; import java.util.concurrent.Future; -import javax.ws.rs.core.Response.Status; - import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; @@ -34,7 +33,8 @@ import org.opendaylight.controller.sal.core.api.data.DataModificationTransaction import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider; import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.controller.sal.restconf.impl.RestconfError; import org.opendaylight.controller.sal.streams.listeners.ListenerAdapter; import org.opendaylight.controller.sal.streams.listeners.Notificator; import org.opendaylight.yangtools.concepts.ListenerRegistration; @@ -120,7 +120,7 @@ public class BrokerFacadeTest { assertSame( "readOperationalDataBehindMountPoint", dataNode, actualNode ); } - @Test(expected=ResponseException.class) + @Test(expected=RestconfDocumentedException.class) public void testReadOperationalDataWithNoDataBroker() { brokerFacade.setDataService( null ); @@ -129,26 +129,19 @@ public class BrokerFacadeTest { @SuppressWarnings("unchecked") @Test - public void testInvokeRpc() { + public void testInvokeRpc() throws Exception { RpcResult expResult = mock( RpcResult.class ); Future> future = Futures.immediateFuture( expResult ); when( mockConsumerSession.rpc( qname, dataNode ) ).thenReturn( future ); - RpcResult actualResult = brokerFacade.invokeRpc( qname, dataNode ); + Future> actualFuture = brokerFacade.invokeRpc( qname, dataNode ); + assertNotNull( "Future is null", actualFuture ); + RpcResult actualResult = actualFuture.get(); assertSame( "invokeRpc", expResult, actualResult ); } - @Test(expected=ResponseException.class) - public void testInvokeRpcWithException() { - Exception mockEx = new Exception( "mock" ); - Future> future = Futures.immediateFailedFuture( mockEx ); - when( mockConsumerSession.rpc( qname, dataNode ) ).thenReturn( future ); - - brokerFacade.invokeRpc( qname, dataNode ); - } - - @Test(expected=ResponseException.class) + @Test(expected=RestconfDocumentedException.class) public void testInvokeRpcWithNoConsumerSession() { brokerFacade.setContext( null ); @@ -218,7 +211,7 @@ public class BrokerFacadeTest { inOrder.verify( mockTransaction ).commit(); } - @Test(expected=ResponseException.class) + @Test(expected=RestconfDocumentedException.class) public void testCommitConfigurationDataPostAlreadyExists() { when( dataBroker.beginTransaction() ).thenReturn( mockTransaction ); mockTransaction.putConfigurationData( instanceID, dataNode ); @@ -226,10 +219,10 @@ public class BrokerFacadeTest { .thenReturn( dataNode ); try { brokerFacade.commitConfigurationDataPost( instanceID, dataNode ); - } catch (ResponseException e) { - assertEquals("Unexpect Exception Status -> " - + "http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48", - (e.getResponse().getStatus()), Status.CONFLICT.getStatusCode()); + } + catch (RestconfDocumentedException e) { + assertEquals("getErrorTag", + RestconfError.ErrorTag.DATA_EXISTS, e.getErrors().get( 0 ).getErrorTag()); throw e; } } @@ -259,7 +252,7 @@ public class BrokerFacadeTest { inOrder.verify( mockTransaction ).commit(); } - @Test(expected=ResponseException.class) + @Test(expected=RestconfDocumentedException.class) public void testCommitConfigurationDataPostBehindMountPointAlreadyExists() { when( mockMountInstance.beginTransaction() ).thenReturn( mockTransaction ); @@ -269,10 +262,10 @@ public class BrokerFacadeTest { try { brokerFacade.commitConfigurationDataPostBehindMountPoint( mockMountInstance, instanceID, dataNode ); - } catch (ResponseException e) { - assertEquals("Unexpect Exception Status -> " - + "http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48", - e.getResponse().getStatus(), Status.CONFLICT.getStatusCode()); + } + catch (RestconfDocumentedException e) { + assertEquals("getErrorTag", + RestconfError.ErrorTag.DATA_EXISTS, e.getErrors().get( 0 ).getErrorTag()); throw e; } } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/CodecsExceptionsCatchingTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/CodecsExceptionsCatchingTest.java index 767aaf36c1..51687e2a12 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/CodecsExceptionsCatchingTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/CodecsExceptionsCatchingTest.java @@ -14,6 +14,7 @@ import org.glassfish.jersey.test.JerseyTest; import org.junit.BeforeClass; import org.junit.Test; import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider; +import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper; import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider; import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider; import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider; @@ -46,6 +47,7 @@ public class CodecsExceptionsCatchingTest extends JerseyTest { resourceConfig = resourceConfig.registerInstances(restConf, StructuredDataToXmlProvider.INSTANCE, StructuredDataToJsonProvider.INSTANCE, XmlToCompositeNodeProvider.INSTANCE, JsonToCompositeNodeProvider.INSTANCE); + resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class ); return resourceConfig; } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java index 018a235718..c0c86c3f25 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java @@ -22,28 +22,30 @@ import static org.mockito.Mockito.when; import java.io.FileNotFoundException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; -import javax.ws.rs.core.Response.Status; - import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import org.opendaylight.controller.sal.common.util.RpcErrors; +import org.opendaylight.controller.sal.common.util.Rpcs; import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; import org.opendaylight.controller.sal.restconf.impl.ControllerContext; import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.controller.sal.restconf.impl.RestconfError; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; import org.opendaylight.controller.sal.restconf.impl.RestconfImpl; import org.opendaylight.controller.sal.restconf.impl.StructuredData; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.RpcError; +import org.opendaylight.yangtools.yang.common.RpcError.ErrorSeverity; import org.opendaylight.yangtools.yang.common.RpcResult; import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.data.api.ModifyAction; @@ -54,6 +56,8 @@ import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; public class InvokeRpcMethodTest { @@ -61,13 +65,6 @@ public class InvokeRpcMethodTest { private RestconfImpl restconfImpl = null; private static ControllerContext controllerContext = null; - private class AnswerImpl implements Answer> { - @Override - public RpcResult answer(final InvocationOnMock invocation) throws Throwable { - CompositeNode compNode = (CompositeNode) invocation.getArguments()[1]; - return new DummyRpcResult.Builder().result(compNode).isSuccessful(true).build(); - } - } @BeforeClass public static void init() throws FileNotFoundException { @@ -111,9 +108,13 @@ public class InvokeRpcMethodTest { restconf.setBroker(mockedBrokerFacade); restconf.setControllerContext(contContext); - when(mockedBrokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))).thenAnswer(new AnswerImpl()); + CompositeNode payload = preparePayload(); + + when(mockedBrokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))) + .thenReturn( Futures.>immediateFuture( + Rpcs.getRpcResult( true ) ) ); - StructuredData structData = restconf.invokeRpc("invoke-rpc-module:rpc-test", preparePayload()); + StructuredData structData = restconf.invokeRpc("invoke-rpc-module:rpc-test", payload); assertTrue(structData == null); } @@ -131,74 +132,90 @@ public class InvokeRpcMethodTest { @Test public void testInvokeRpcWithNoPayloadRpc_FailNoErrors() { - RpcResult rpcResult = mock(RpcResult.class); - when(rpcResult.isSuccessful()).thenReturn(false); + RpcResult rpcResult = Rpcs.getRpcResult( false ); - ArgumentCaptor payload = ArgumentCaptor - .forClass(CompositeNode.class); BrokerFacade brokerFacade = mock(BrokerFacade.class); - when( - brokerFacade.invokeRpc( - eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), - payload.capture())).thenReturn(rpcResult); + when( brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), + any(CompositeNode.class))) + .thenReturn( Futures.>immediateFuture( rpcResult ) ); restconfImpl.setBroker(brokerFacade); try { restconfImpl.invokeRpc("toaster:cancel-toast", ""); fail("Expected an exception to be thrown."); - } catch (ResponseException e) { - assertEquals(e.getMessage(), - Status.INTERNAL_SERVER_ERROR.getStatusCode(), e - .getResponse().getStatus()); + } + catch (RestconfDocumentedException e) { + verifyRestconfDocumentedException( e, 0, ErrorType.RPC, ErrorTag.OPERATION_FAILED, + Optional.absent(), Optional.absent() ); } } - @Test - public void testInvokeRpcWithNoPayloadRpc_FailWithRpcError() { - List rpcErrors = new LinkedList(); + void verifyRestconfDocumentedException( final RestconfDocumentedException e, final int index, + final ErrorType expErrorType, final ErrorTag expErrorTag, + final Optional expErrorMsg, + final Optional expAppTag ) { + RestconfError actual = null; + try { + actual = e.getErrors().get( index ); + } + catch( ArrayIndexOutOfBoundsException ex ) { + fail( "RestconfError not found at index " + index ); + } + + assertEquals( "getErrorType", expErrorType, actual.getErrorType() ); + assertEquals( "getErrorTag", expErrorTag, actual.getErrorTag() ); + assertNotNull( "getErrorMessage is null", actual.getErrorMessage() ); + + if( expErrorMsg.isPresent() ) { + assertEquals( "getErrorMessage", expErrorMsg.get(), actual.getErrorMessage() ); + } - RpcError unknownError = mock(RpcError.class); - when( unknownError.getTag() ).thenReturn( "bogusTag" ); - rpcErrors.add( unknownError ); + if( expAppTag.isPresent() ) { + assertEquals( "getErrorAppTag", expAppTag.get(), actual.getErrorAppTag() ); + } + } - RpcError knownError = mock( RpcError.class ); - when( knownError.getTag() ).thenReturn( "in-use" ); - rpcErrors.add( knownError ); + @Test + public void testInvokeRpcWithNoPayloadRpc_FailWithRpcError() { + List rpcErrors = Arrays.asList( + RpcErrors.getRpcError( null, "bogusTag", null, ErrorSeverity.ERROR, "foo", + RpcError.ErrorType.TRANSPORT, null ), + RpcErrors.getRpcError( "app-tag", "in-use", null, ErrorSeverity.WARNING, "bar", + RpcError.ErrorType.RPC, null )); - RpcResult rpcResult = mock(RpcResult.class); - when(rpcResult.isSuccessful()).thenReturn(false); - when(rpcResult.getErrors()).thenReturn( rpcErrors ); + RpcResult rpcResult = Rpcs.getRpcResult( false, rpcErrors ); - ArgumentCaptor payload = ArgumentCaptor - .forClass(CompositeNode.class); BrokerFacade brokerFacade = mock(BrokerFacade.class); - when( - brokerFacade.invokeRpc( - eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), - payload.capture())).thenReturn(rpcResult); + when( brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), + any(CompositeNode.class))) + .thenReturn( Futures.>immediateFuture( rpcResult ) ); restconfImpl.setBroker(brokerFacade); try { restconfImpl.invokeRpc("toaster:cancel-toast", ""); fail("Expected an exception to be thrown."); - } catch (ResponseException e) { - //TODO: Change to a 409 in the future - waiting on additional BUG to enhance this. - assertEquals(e.getMessage(), 500, e.getResponse().getStatus()); + } + catch (RestconfDocumentedException e) { + verifyRestconfDocumentedException( e, 0, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED, + Optional.of( "foo" ), Optional.absent() ); + verifyRestconfDocumentedException( e, 1, ErrorType.RPC, ErrorTag.IN_USE, + Optional.of( "bar" ), Optional.of( "app-tag" ) ); } } @Test public void testInvokeRpcWithNoPayload_Success() { - RpcResult rpcResult = mock(RpcResult.class); - when(rpcResult.isSuccessful()).thenReturn(true); + RpcResult rpcResult = Rpcs.getRpcResult( true ); BrokerFacade brokerFacade = mock(BrokerFacade.class); - when( - brokerFacade.invokeRpc( - eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), - any( CompositeNode.class ))).thenReturn(rpcResult); + when( brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")), + any( CompositeNode.class ))) + .thenReturn( Futures.>immediateFuture( rpcResult ) ); restconfImpl.setBroker(brokerFacade); @@ -213,10 +230,9 @@ public class InvokeRpcMethodTest { try { restconfImpl.invokeRpc("toaster:cancel-toast", " a payload "); fail("Expected an exception"); - } catch (ResponseException e) { - assertEquals(e.getMessage(), - Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode(), e - .getResponse().getStatus()); + } catch (RestconfDocumentedException e) { + verifyRestconfDocumentedException( e, 0, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, + Optional.absent(), Optional.absent() ); } } @@ -225,24 +241,24 @@ public class InvokeRpcMethodTest { try { restconfImpl.invokeRpc("toaster:bad-method", ""); fail("Expected an exception"); - } catch (ResponseException e) { - assertEquals(e.getMessage(), Status.NOT_FOUND.getStatusCode(), e - .getResponse().getStatus()); + } + catch (RestconfDocumentedException e) { + verifyRestconfDocumentedException( e, 0, ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT, + Optional.absent(), Optional.absent() ); } } @Test public void testInvokeRpcMethodWithInput() { - RpcResult rpcResult = mock(RpcResult.class); - when(rpcResult.isSuccessful()).thenReturn(true); + RpcResult rpcResult = Rpcs.getRpcResult( true ); CompositeNode payload = mock(CompositeNode.class); BrokerFacade brokerFacade = mock(BrokerFacade.class); - when( - brokerFacade.invokeRpc( - eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)make-toast")), - any(CompositeNode.class))).thenReturn(rpcResult); + when( brokerFacade.invokeRpc( + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)make-toast")), + any(CompositeNode.class))) + .thenReturn( Futures.>immediateFuture( rpcResult ) ); restconfImpl.setBroker(brokerFacade); @@ -257,29 +273,28 @@ public class InvokeRpcMethodTest { try { restconfImpl.invokeRpc("toaster/slash", ""); fail("Expected an exception."); - } catch (ResponseException e) { - assertEquals(e.getMessage(), Status.NOT_FOUND.getStatusCode(), e - .getResponse().getStatus()); + } + catch (RestconfDocumentedException e) { + verifyRestconfDocumentedException( e, 0, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, + Optional.absent(), Optional.absent() ); } } @Test public void testInvokeRpcWithNoPayloadWithOutput_Success() { - RpcResult rpcResult = mock(RpcResult.class); - when(rpcResult.isSuccessful()).thenReturn(true); - CompositeNode compositeNode = mock( CompositeNode.class ); - when( rpcResult.getResult() ).thenReturn( compositeNode ); + RpcResult rpcResult = Rpcs.getRpcResult( true, compositeNode, + Collections.emptyList() ); BrokerFacade brokerFacade = mock(BrokerFacade.class); when( brokerFacade.invokeRpc( - eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)testOutput")), - any( CompositeNode.class ))).thenReturn(rpcResult); + eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)testOutput")), + any( CompositeNode.class ))) + .thenReturn( Futures.>immediateFuture( rpcResult ) ); restconfImpl.setBroker(brokerFacade); - StructuredData output = restconfImpl.invokeRpc("toaster:testOutput", - ""); + StructuredData output = restconfImpl.invokeRpc("toaster:testOutput", ""); assertNotNull( output ); assertSame( compositeNode, output.getData() ); assertNotNull( output.getSchema() ); @@ -288,8 +303,7 @@ public class InvokeRpcMethodTest { @Test public void testMountedRpcCallNoPayload_Success() throws Exception { - RpcResult rpcResult = mock(RpcResult.class); - when(rpcResult.isSuccessful()).thenReturn(true); + RpcResult rpcResult = Rpcs.getRpcResult( true ); ListenableFuture> mockListener = mock( ListenableFuture.class ); when( mockListener.get() ).thenReturn( rpcResult ); @@ -321,6 +335,4 @@ public class InvokeRpcMethodTest { //additional validation in the fact that the restconfImpl does not throw an exception. } - - } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/NormalizeNodeTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/NormalizeNodeTest.java index 6d2723c2f1..f5aa453fa2 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/NormalizeNodeTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/NormalizeNodeTest.java @@ -15,7 +15,7 @@ import java.net.URISyntaxException; import org.junit.BeforeClass; import org.junit.Test; import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.controller.sal.restconf.impl.SimpleNodeWrapper; import org.opendaylight.yangtools.yang.data.api.CompositeNode; @@ -26,48 +26,28 @@ public class NormalizeNodeTest extends YangAndXmlAndDataSchemaLoader { dataLoad("/normalize-node/yang/"); } - @Test + @Test(expected=RestconfDocumentedException.class) public void namespaceNotNullAndInvalidNamespaceAndNoModuleNameTest() { - boolean exceptionReised = false; - try { - TestUtils.normalizeCompositeNode(prepareCnSn("wrongnamespace"), modules, schemaNodePath); - } catch (ResponseException e) { - exceptionReised = true; - } - assertTrue(exceptionReised); + + TestUtils.normalizeCompositeNode(prepareCnSn("wrongnamespace"), modules, schemaNodePath); } @Test public void namespaceNullTest() { - String exceptionMessage = null; - try { - TestUtils.normalizeCompositeNode(prepareCnSn(null), modules, schemaNodePath); - } catch (ResponseException e) { - exceptionMessage = String.valueOf(e.getResponse().getEntity()); - } - assertNull(exceptionMessage); + + TestUtils.normalizeCompositeNode(prepareCnSn(null), modules, schemaNodePath); } @Test public void namespaceValidNamespaceTest() { - String exceptionMessage = null; - try { - TestUtils.normalizeCompositeNode(prepareCnSn("normalize:node:module"), modules, schemaNodePath); - } catch (ResponseException e) { - exceptionMessage = String.valueOf(e.getResponse().getEntity()); - } - assertNull(exceptionMessage); + + TestUtils.normalizeCompositeNode(prepareCnSn("normalize:node:module"), modules, schemaNodePath); } @Test public void namespaceValidModuleNameTest() { - String exceptionMessage = null; - try { - TestUtils.normalizeCompositeNode(prepareCnSn("normalize-node-module"), modules, schemaNodePath); - } catch (ResponseException e) { - exceptionMessage = String.valueOf(e.getResponse().getEntity()); - } - assertNull(exceptionMessage); + + TestUtils.normalizeCompositeNode(prepareCnSn("normalize-node-module"), modules, schemaNodePath); } private CompositeNode prepareCnSn(String namespace) { diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetAugmentedElementWhenEqualNamesTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetAugmentedElementWhenEqualNamesTest.java index a6391894c2..53183c611c 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetAugmentedElementWhenEqualNamesTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetAugmentedElementWhenEqualNamesTest.java @@ -9,6 +9,7 @@ package org.opendaylight.controller.sal.restconf.impl.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.FileNotFoundException; @@ -18,16 +19,16 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.opendaylight.controller.sal.restconf.impl.ControllerContext; import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.yangtools.yang.model.api.SchemaContext; public class RestGetAugmentedElementWhenEqualNamesTest { - + private static ControllerContext controllerContext = ControllerContext.getInstance(); - + @Rule public ExpectedException exception = ExpectedException.none(); - + @BeforeClass public static void init() throws FileNotFoundException { SchemaContext schemaContextTestModule = TestUtils.loadSchemaContext("/common/augment/yang"); @@ -41,17 +42,15 @@ public class RestGetAugmentedElementWhenEqualNamesTest { iiWithData = controllerContext.toInstanceIdentifier("main:cont/augment-main-b:cont1"); assertEquals("ns:augment:main:b", iiWithData.getSchemaNode().getQName().getNamespace().toString()); } - + @Test public void nodeWithoutNamespaceHasMoreAugments() { - boolean exceptionCaught = false; try { controllerContext.toInstanceIdentifier("main:cont/cont1"); - } catch (ResponseException e) { - assertTrue(((String) e.getResponse().getEntity()).contains("is added as augment from more than one module")); - exceptionCaught = true; + fail( "Expected exception" ); + } catch (RestconfDocumentedException e) { + assertTrue(e.getErrors().get( 0 ).getErrorMessage().contains( + "is added as augment from more than one module")); } - assertTrue(exceptionCaught); } - } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetOperationTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetOperationTest.java index 4198e20b83..893622f60a 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetOperationTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetOperationTest.java @@ -34,11 +34,11 @@ import javax.ws.rs.core.Response; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.core.api.mount.MountService; import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider; +import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper; import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider; import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider; import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider; @@ -92,6 +92,7 @@ public class RestGetOperationTest extends JerseyTest { resourceConfig = resourceConfig.registerInstances(restconfImpl, StructuredDataToXmlProvider.INSTANCE, StructuredDataToJsonProvider.INSTANCE, XmlToCompositeNodeProvider.INSTANCE, JsonToCompositeNodeProvider.INSTANCE); + resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class ); return resourceConfig; } @@ -145,15 +146,15 @@ public class RestGetOperationTest extends JerseyTest { /** * MountPoint test. URI represents mount point. - * + * * Slashes in URI behind mount point. lst1 element with key * GigabitEthernet0%2F0%2F0%2F0 (GigabitEthernet0/0/0/0) is requested via * GET HTTP operation. It is tested whether %2F character is replaced with * simple / in InstanceIdentifier parameter in method * {@link BrokerFacade#readConfigurationDataBehindMountPoint(MountInstance, InstanceIdentifier)} * which is called in method {@link RestconfImpl#readConfigurationData} - * - * + * + * * @throws ParseException */ @Test diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestPostOperationTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestPostOperationTest.java index c6e2f14343..ce460fe474 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestPostOperationTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestPostOperationTest.java @@ -38,6 +38,7 @@ import org.opendaylight.controller.sal.core.api.mount.MountInstance; import org.opendaylight.controller.sal.core.api.mount.MountService; import org.opendaylight.controller.sal.rest.api.Draft02; import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider; +import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper; import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider; import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider; import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider; @@ -52,6 +53,8 @@ import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import com.google.common.util.concurrent.Futures; + public class RestPostOperationTest extends JerseyTest { private static String xmlDataAbsolutePath; @@ -99,6 +102,7 @@ public class RestPostOperationTest extends JerseyTest { resourceConfig = resourceConfig.registerInstances(restconfImpl, StructuredDataToXmlProvider.INSTANCE, StructuredDataToJsonProvider.INSTANCE, XmlToCompositeNodeProvider.INSTANCE, JsonToCompositeNodeProvider.INSTANCE); + resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class ); return resourceConfig; } @@ -116,7 +120,7 @@ public class RestPostOperationTest extends JerseyTest { assertEquals(500, post(uri, MediaType.APPLICATION_XML, xmlDataRpcInput)); uri = "/operations/test-module:rpc-wrongtest"; - assertEquals(404, post(uri, MediaType.APPLICATION_XML, xmlDataRpcInput)); + assertEquals(400, post(uri, MediaType.APPLICATION_XML, xmlDataRpcInput)); } @Test @@ -173,7 +177,8 @@ public class RestPostOperationTest extends JerseyTest { private void mockInvokeRpc(CompositeNode result, boolean sucessful) { RpcResult rpcResult = new DummyRpcResult.Builder().result(result) .isSuccessful(sucessful).build(); - when(brokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))).thenReturn(rpcResult); + when(brokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))) + .thenReturn(Futures.>immediateFuture( rpcResult )); } private void mockCommitConfigurationDataPostMethod(TransactionStatus statusName) { diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java new file mode 100644 index 0000000000..fc5d7be724 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java @@ -0,0 +1,966 @@ +/* + * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.sal.restconf.impl.test; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.controller.sal.rest.api.Draft02; +import org.opendaylight.controller.sal.rest.api.RestconfService; +import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper; +import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider; +import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider; +import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.controller.sal.restconf.impl.RestconfError; +import org.opendaylight.controller.sal.restconf.impl.StructuredData; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.io.ByteStreams; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** + * Unit tests for RestconfDocumentedExceptionMapper. + * + * @author Thomas Pantelis + */ +public class RestconfDocumentedExceptionMapperTest extends JerseyTest { + + interface ErrorInfoVerifier { + void verifyXML( Node errorInfoNode ); + void verifyJson( JsonElement errorInfoElement ); + } + + static class ComplexErrorInfoVerifier implements ErrorInfoVerifier { + + Map expErrorInfo; + + public ComplexErrorInfoVerifier( Map expErrorInfo ) { + this.expErrorInfo = expErrorInfo; + } + + @Override + public void verifyXML( Node errorInfoNode ) { + + Map mutableExpMap = Maps.newHashMap( expErrorInfo ); + NodeList childNodes = errorInfoNode.getChildNodes(); + for( int i = 0; i < childNodes.getLength(); i++ ) { + Node child = childNodes.item( i ); + if( child instanceof Element ) { + String expValue = mutableExpMap.remove( child.getNodeName() ); + assertNotNull( "Found unexpected \"error-info\" child node: " + + child.getNodeName(), expValue ); + assertEquals( "Text content for \"error-info\" child node " + + child.getNodeName(), expValue, child.getTextContent() ); + } + } + + if( !mutableExpMap.isEmpty() ) { + fail( "Missing \"error-info\" child nodes: " + mutableExpMap ); + } + } + + @Override + public void verifyJson( JsonElement errorInfoElement ) { + + assertTrue( "\"error-info\" Json element is not an Object", + errorInfoElement.isJsonObject() ); + + Map actualErrorInfo = Maps.newHashMap(); + for( Entry entry: errorInfoElement.getAsJsonObject().entrySet() ) { + String leafName = entry.getKey(); + JsonElement leafElement = entry.getValue(); + actualErrorInfo.put( leafName, leafElement.getAsString() ); + } + + Map mutableExpMap = Maps.newHashMap( expErrorInfo ); + for( Entry actual: actualErrorInfo.entrySet() ) { + String expValue = mutableExpMap.remove( actual.getKey() ); + assertNotNull( "Found unexpected \"error-info\" child node: " + + actual.getKey(), expValue ); + assertEquals( "Text content for \"error-info\" child node " + + actual.getKey(), expValue, actual.getValue() ); + } + + if( !mutableExpMap.isEmpty() ) { + fail( "Missing \"error-info\" child nodes: " + mutableExpMap ); + } + } + } + + static class SimpleErrorInfoVerifier implements ErrorInfoVerifier { + + String expTextContent; + + public SimpleErrorInfoVerifier( String expErrorInfo ) { + this.expTextContent = expErrorInfo; + } + + void verifyContent( String actualContent ) { + assertNotNull( "Actual \"error-info\" text content is null", actualContent ); + assertTrue( "", actualContent.contains( expTextContent ) ); + } + + @Override + public void verifyXML( Node errorInfoNode ) { + verifyContent( errorInfoNode.getTextContent() ); + } + + @Override + public void verifyJson( JsonElement errorInfoElement ) { + verifyContent( errorInfoElement.getAsString() ); + } + } + + static RestconfService mockRestConf = mock( RestconfService.class ); + + static XPath XPATH = XPathFactory.newInstance().newXPath(); + static XPathExpression ERROR_LIST; + static XPathExpression ERROR_TYPE; + static XPathExpression ERROR_TAG; + static XPathExpression ERROR_MESSAGE; + static XPathExpression ERROR_APP_TAG; + static XPathExpression ERROR_INFO; + + @BeforeClass + public static void init() throws Exception { + ControllerContext.getInstance().setGlobalSchema( TestUtils.loadSchemaContext("/modules") ); + + NamespaceContext nsContext = new NamespaceContext() { + @Override + public Iterator getPrefixes( String namespaceURI ) { + return null; + } + + @Override + public String getPrefix( String namespaceURI ) { + return null; + } + + @Override + public String getNamespaceURI( String prefix ) { + return "ietf-restconf".equals( prefix ) ? Draft02.RestConfModule.NAMESPACE : null; + } + }; + + XPATH.setNamespaceContext( nsContext ); + ERROR_LIST = XPATH.compile( "ietf-restconf:errors/ietf-restconf:error" ); + ERROR_TYPE = XPATH.compile( "ietf-restconf:error-type" ); + ERROR_TAG = XPATH.compile( "ietf-restconf:error-tag" ); + ERROR_MESSAGE = XPATH.compile( "ietf-restconf:error-message" ); + ERROR_APP_TAG = XPATH.compile( "ietf-restconf:error-app-tag" ); + ERROR_INFO = XPATH.compile( "ietf-restconf:error-info" ); + } + + @Override + @Before + public void setUp() throws Exception { + reset( mockRestConf ); + super.setUp(); + } + + @Override + protected Application configure() { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig = resourceConfig.registerInstances( mockRestConf, StructuredDataToXmlProvider.INSTANCE, + StructuredDataToJsonProvider.INSTANCE ); + resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class ); + return resourceConfig; + } + + void stageMockEx( RestconfDocumentedException ex ) { + reset( mockRestConf ); + when( mockRestConf.readOperationalData( any( String.class ) ) ).thenThrow( ex ); + } + + void testJsonResponse( RestconfDocumentedException ex, Status expStatus, ErrorType expErrorType, + ErrorTag expErrorTag, String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) throws Exception { + + stageMockEx( ex ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, expStatus ); + + verifyJsonResponseBody( stream, expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + @Test + public void testToJsonResponseWithMessageOnly() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error" ), Status.INTERNAL_SERVER_ERROR, + ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", null, null ); + + // To test verification code +// String json = +// "{ errors: {" + +// " error: [{" + +// " error-tag : \"operation-failed\"" + +// " ,error-type : \"application\"" + +// " ,error-message : \"An error occurred\"" + +// " ,error-info : {" + +// " session-id: \"123\"" + +// " ,address: \"1.2.3.4\"" + +// " }" + +// " }]" + +// " }" + +// "}"; +// +// verifyJsonResponseBody( new java.io.StringBufferInputStream(json ), ErrorType.APPLICATION, +// ErrorTag.OPERATION_FAILED, "An error occurred", null, +// com.google.common.collect.ImmutableMap.of( "session-id", "123", "address", "1.2.3.4" ) ); + } + + @Test + public void testToJsonResponseWithInUseErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.IN_USE ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.IN_USE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithInvalidValueErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.RPC, + ErrorTag.INVALID_VALUE ), + Status.BAD_REQUEST, ErrorType.RPC, + ErrorTag.INVALID_VALUE, "mock error", null, null ); + + } + + @Test + public void testToJsonResponseWithTooBigErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.TRANSPORT, + ErrorTag.TOO_BIG ), + Status.REQUEST_ENTITY_TOO_LARGE, ErrorType.TRANSPORT, + ErrorTag.TOO_BIG, "mock error", null, null ); + + } + + @Test + public void testToJsonResponseWithMissingAttributeErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithBadAttributeErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE, "mock error", null, null ); + } + @Test + public void testToJsonResponseWithUnknownAttributeErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithBadElementErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT ), + Status.BAD_REQUEST, + ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithUnknownElementErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithUnknownNamespaceErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithMalformedMessageErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithAccessDeniedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED ), + Status.FORBIDDEN, ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithLockDeniedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithResourceDeniedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithRollbackFailedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithDataExistsErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithDataMissingErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithOperationNotSupportedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED ), + Status.NOT_IMPLEMENTED, ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithOperationFailedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithPartialOperationErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithErrorAppTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag" ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", null ); + } + + @Test + public void testToJsonResponseWithMultipleErrors() throws Exception { + + List errorList = Arrays.asList( + new RestconfError( ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, "mock error1" ), + new RestconfError( ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2" ) ); + stageMockEx( new RestconfDocumentedException( errorList ) ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, Status.CONFLICT ); + + JsonArray arrayElement = parseJsonErrorArrayElement( stream ); + + assertEquals( "\"error\" Json array element length", 2, arrayElement.size() ); + + verifyJsonErrorNode( arrayElement.get( 0 ), ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, + "mock error1", null, null ); + + verifyJsonErrorNode( arrayElement.get( 1 ), ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, + "mock error2", null, null ); + } + + @Test + public void testToJsonResponseWithErrorInfo() throws Exception { + + String errorInfo = "
1.2.3.4
123"; + testJsonResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag", errorInfo ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", + new ComplexErrorInfoVerifier( ImmutableMap.of( + "session-id", "123", "address", "1.2.3.4" ) ) ); + } + + @Test + public void testToJsonResponseWithExceptionCause() throws Exception { + + Exception cause = new Exception( "mock exception cause" ); + testJsonResponse( new RestconfDocumentedException( "mock error", cause ), + Status.INTERNAL_SERVER_ERROR, ErrorType.APPLICATION, + ErrorTag.OPERATION_FAILED, "mock error", null, + new SimpleErrorInfoVerifier( cause.getMessage() ) ); + } + + void testXMLResponse( RestconfDocumentedException ex, Status expStatus, ErrorType expErrorType, + ErrorTag expErrorTag, String expErrorMessage, + String expErrorAppTag, ErrorInfoVerifier errorInfoVerifier ) throws Exception + { + stageMockEx( ex ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_XML ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_XML, expStatus ); + + verifyXMLResponseBody( stream, expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + @Test + public void testToXMLResponseWithMessageOnly() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error" ), Status.INTERNAL_SERVER_ERROR, + ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", null, null ); + + // To test verification code +// String xml = +// ""+ +// " " + +// " application"+ +// " operation-failed"+ +// " An error occurred"+ +// " " + +// " 123" + +// "
1.2.3.4
" + +// "
" + +// "
" + +// "
"; +// +// verifyXMLResponseBody( new java.io.StringBufferInputStream(xml), ErrorType.APPLICATION, +// ErrorTag.OPERATION_FAILED, "An error occurred", null, +// com.google.common.collect.ImmutableMap.of( "session-id", "123", "address", "1.2.3.4" ) ); + } + + @Test + public void testToXMLResponseWithInUseErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.IN_USE ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.IN_USE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithInvalidValueErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.RPC, + ErrorTag.INVALID_VALUE ), + Status.BAD_REQUEST, ErrorType.RPC, + ErrorTag.INVALID_VALUE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithTooBigErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.TRANSPORT, + ErrorTag.TOO_BIG ), + Status.REQUEST_ENTITY_TOO_LARGE, ErrorType.TRANSPORT, + ErrorTag.TOO_BIG, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithMissingAttributeErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithBadAttributeErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE, "mock error", null, null ); + } + @Test + public void testToXMLResponseWithUnknownAttributeErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithBadElementErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithUnknownElementErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithUnknownNamespaceErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithMalformedMessageErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithAccessDeniedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED ), + Status.FORBIDDEN, ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithLockDeniedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithResourceDeniedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithRollbackFailedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithDataExistsErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithDataMissingErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithOperationNotSupportedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED ), + Status.NOT_IMPLEMENTED, ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithOperationFailedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithPartialOperationErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithErrorAppTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag" ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", null ); + } + + @Test + public void testToXMLResponseWithErrorInfo() throws Exception { + + String errorInfo = "
1.2.3.4
123"; + testXMLResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag", errorInfo ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", + new ComplexErrorInfoVerifier( ImmutableMap.of( + "session-id", "123", "address", "1.2.3.4" ) ) ); + } + + @Test + public void testToXMLResponseWithExceptionCause() throws Exception { + + Exception cause = new Exception( "mock exception cause" ); + testXMLResponse( new RestconfDocumentedException( "mock error", cause ), + Status.INTERNAL_SERVER_ERROR, ErrorType.APPLICATION, + ErrorTag.OPERATION_FAILED, "mock error", null, + new SimpleErrorInfoVerifier( cause.getMessage() ) ); + } + + @Test + public void testToXMLResponseWithMultipleErrors() throws Exception { + + List errorList = Arrays.asList( + new RestconfError( ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, "mock error1" ), + new RestconfError( ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2" ) ); + stageMockEx( new RestconfDocumentedException( errorList ) ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_XML ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_XML, Status.CONFLICT ); + + Document doc = parseXMLDocument( stream ); + + NodeList children = getXMLErrorList( doc, 2 ); + + verifyXMLErrorNode( children.item( 0 ), ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, + "mock error1", null, null ); + + verifyXMLErrorNode( children.item( 1 ), ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, + "mock error2", null, null ); + } + + @Test + public void testToResponseWithAcceptHeader() throws Exception { + + stageMockEx( new RestconfDocumentedException( "mock error" ) ); + + Response resp = target("/operational/foo") + .request().header( "Accept", MediaType.APPLICATION_JSON ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, + Status.INTERNAL_SERVER_ERROR ); + + verifyJsonResponseBody( stream, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", + null, null ); + } + + @Test + public void testToResponseWithStatusOnly() throws Exception { + + // The StructuredDataToJsonProvider should throw a RestconfDocumentedException with no data + + when( mockRestConf.readOperationalData( any( String.class ) ) ) + .thenReturn( new StructuredData( null, null, null ) ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); + + verifyResponse( resp, MediaType.TEXT_PLAIN, Status.NOT_FOUND ); + } + + InputStream verifyResponse( Response resp, String expMediaType, Status expStatus ) { + assertEquals( "getMediaType", MediaType.valueOf( expMediaType ), resp.getMediaType() ); + assertEquals( "getStatus", expStatus.getStatusCode(), resp.getStatus() ); + + Object entity = resp.getEntity(); + assertEquals( "Response entity", true, entity instanceof InputStream ); + InputStream stream = (InputStream)entity; + return stream; + } + + void verifyJsonResponseBody( InputStream stream, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) throws Exception { + + JsonArray arrayElement = parseJsonErrorArrayElement( stream ); + + assertEquals( "\"error\" Json array element length", 1, arrayElement.size() ); + + verifyJsonErrorNode( arrayElement.get( 0 ), expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + private JsonArray parseJsonErrorArrayElement( InputStream stream ) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteStreams.copy( stream, bos ); + + System.out.println("JSON: "+bos.toString()); + + JsonParser parser = new JsonParser(); + JsonElement rootElement; + + try { + rootElement = parser.parse( + new InputStreamReader( new ByteArrayInputStream( bos.toByteArray() ) ) ); + } + catch( Exception e ) { + throw new IllegalArgumentException( "Invalid JSON response:\n" + bos.toString(), e ); + } + + assertTrue( "Root element of Json is not an Object", rootElement.isJsonObject() ); + + Set> errorsEntrySet = rootElement.getAsJsonObject().entrySet(); + assertEquals( "Json Object element set count", 1, errorsEntrySet.size() ); + + Entry errorsEntry = errorsEntrySet.iterator().next(); + JsonElement errorsElement = errorsEntry.getValue(); + assertEquals( "First Json element name", "errors", errorsEntry.getKey() ); + assertTrue( "\"errors\" Json element is not an Object", errorsElement.isJsonObject() ); + + Set> errorListEntrySet = errorsElement.getAsJsonObject().entrySet(); + assertEquals( "Root \"errors\" element child count", 1, errorListEntrySet.size() ); + + JsonElement errorListElement = errorListEntrySet.iterator().next().getValue(); + assertEquals( "\"errors\" child Json element name", "error", + errorListEntrySet.iterator().next().getKey() ); + assertTrue( "\"error\" Json element is not an Array", errorListElement.isJsonArray() ); + + return errorListElement.getAsJsonArray(); + } + + void verifyJsonErrorNode( JsonElement errorEntryElement, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) { + + JsonElement errorInfoElement = null; + Map actualErrorInfo = null; + Map leafMap = Maps.newHashMap(); + for( Entry entry: errorEntryElement.getAsJsonObject().entrySet() ) { + String leafName = entry.getKey(); + JsonElement leafElement = entry.getValue(); + + if( "error-info".equals( leafName ) ) { + assertNotNull( "Found unexpected \"error-info\" element", errorInfoVerifier ); + errorInfoElement = leafElement; + } + else { + assertTrue( "\"error\" leaf Json element " + leafName + + " is not a Primitive", leafElement.isJsonPrimitive() ); + + leafMap.put( leafName, leafElement.getAsString() ); + } + } + + assertEquals( "error-type", expErrorType.getErrorTypeTag(), leafMap.remove( "error-type" ) ); + assertEquals( "error-tag", expErrorTag.getTagValue(), leafMap.remove( "error-tag" ) ); + + verifyOptionalJsonLeaf( leafMap.remove( "error-message" ), expErrorMessage, "error-message" ); + verifyOptionalJsonLeaf( leafMap.remove( "error-app-tag" ), expErrorAppTag, "error-app-tag" ); + + if( !leafMap.isEmpty() ) { + fail( "Found unexpected Json leaf elements for \"error\" element: " + leafMap ); + } + + if( errorInfoVerifier != null ) { + assertNotNull( "Missing \"error-info\" element", errorInfoElement ); + errorInfoVerifier.verifyJson( errorInfoElement ); + } + } + + void verifyOptionalJsonLeaf( String actualValue, String expValue, String tagName ) { + if( expValue != null ) { + assertEquals( tagName, expValue, actualValue ); + } + else { + assertNull( "Found unexpected \"error\" leaf entry for: " + tagName, actualValue ); + } + } + + void verifyXMLResponseBody( InputStream stream, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) + throws Exception { + + Document doc = parseXMLDocument( stream ); + + NodeList children = getXMLErrorList( doc, 1 ); + + verifyXMLErrorNode( children.item( 0 ), expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + private Document parseXMLDocument( InputStream stream ) throws IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setCoalescing(true); + factory.setIgnoringElementContentWhitespace(true); + factory.setIgnoringComments(true); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteStreams.copy( stream, bos ); + + System.out.println("XML: "+bos.toString()); + + Document doc = null; + try { + doc = factory.newDocumentBuilder().parse( new ByteArrayInputStream( bos.toByteArray() ) ); + } + catch( Exception e ) { + throw new IllegalArgumentException( "Invalid XML response:\n" + bos.toString(), e ); + } + return doc; + } + + void verifyXMLErrorNode( Node errorNode, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) throws Exception { + + String errorType = (String)ERROR_TYPE.evaluate( errorNode, XPathConstants.STRING ); + assertEquals( "error-type", expErrorType.getErrorTypeTag(), errorType ); + + String errorTag = (String)ERROR_TAG.evaluate( errorNode, XPathConstants.STRING ); + assertEquals( "error-tag", expErrorTag.getTagValue(), errorTag ); + + verifyOptionalXMLLeaf( errorNode, ERROR_MESSAGE, expErrorMessage, "error-message" ); + verifyOptionalXMLLeaf( errorNode, ERROR_APP_TAG, expErrorAppTag, "error-app-tag" ); + + Node errorInfoNode = (Node)ERROR_INFO.evaluate( errorNode, XPathConstants.NODE ); + if( errorInfoVerifier != null ) { + assertNotNull( "Missing \"error-info\" node", errorInfoNode ); + + errorInfoVerifier.verifyXML( errorInfoNode ); + } + else { + assertNull( "Found unexpected \"error-info\" node", errorInfoNode ); + } + } + + void verifyOptionalXMLLeaf( Node fromNode, XPathExpression xpath, String expValue, + String tagName ) throws Exception { + if( expValue != null ) { + String actual = (String)xpath.evaluate( fromNode, XPathConstants.STRING ); + assertEquals( tagName, expValue, actual ); + } + else { + assertNull( "Found unexpected \"error\" leaf entry for: " + tagName, + xpath.evaluate( fromNode, XPathConstants.NODE ) ); + } + } + + NodeList getXMLErrorList( Node fromNode, int count ) throws Exception { + NodeList errorList = (NodeList)ERROR_LIST.evaluate( fromNode, XPathConstants.NODESET ); + assertNotNull( "Root errors node is empty", errorList ); + assertEquals( "Root errors node child count", count, errorList.getLength() ); + return errorList; + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java new file mode 100644 index 0000000000..70ad7683a4 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java @@ -0,0 +1,236 @@ +/* +* Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved. +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License v1.0 which accompanies this distribution, +* and is available at http://www.eclipse.org/legal/epl-v10.html +*/ +package org.opendaylight.controller.sal.restconf.impl.test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Response.Status; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Test; + +import static org.opendaylight.controller.sal.common.util.RpcErrors.getRpcError; + +import org.opendaylight.controller.sal.restconf.impl.RestconfError; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; +import org.opendaylight.yangtools.yang.common.RpcError; + +/** + * Unit tests for RestconfError. + * + * @author Devin Avery + * @author Thomas Pantelis + * + */ +public class RestconfErrorTest { + + static class Contains extends BaseMatcher { + + private final String text; + + public Contains( String text ) { + this.text = text; + } + + @Override + public void describeTo( Description desc ) { + desc.appendText( "contains " ).appendValue( text ); + } + + @Override + public boolean matches( Object arg ) { + return arg != null && arg.toString().contains( text ); + } + } + + @Test + public void testErrorTagValueOf() + { + assertEquals( ErrorTag.IN_USE, + ErrorTag.valueOfCaseInsensitive( ErrorTag.IN_USE.getTagValue() ) ); + } + + @Test + public void testErrorTagValueOfIsLowercase() + { + assertEquals( "in-use", + ErrorTag.IN_USE.getTagValue() ); + } + + @Test + public void testErrorTypeGetErrorTypeTagIsLowerCase() + { + assertEquals( ErrorType.APPLICATION.name().toLowerCase(), + ErrorType.APPLICATION.getErrorTypeTag() ); + } + + @Test + public void testErrorTypeValueOf() + { + assertEquals( ErrorType.APPLICATION, + ErrorType.valueOfCaseInsensitive( ErrorType.APPLICATION.getErrorTypeTag() ) ); + } + + @Test + public void testErrorTagStatusCodes() + { + Map lookUpMap = new HashMap(); + + lookUpMap.put( "in-use", Status.fromStatusCode(409)); + lookUpMap.put( "invalid-value", Status.fromStatusCode(400)); + lookUpMap.put( "too-big", Status.fromStatusCode(413)); + lookUpMap.put( "missing-attribute", Status.fromStatusCode(400)); + lookUpMap.put( "bad-attribute", Status.fromStatusCode(400)); + lookUpMap.put( "unknown-attribute", Status.fromStatusCode(400)); + lookUpMap.put( "bad-element", Status.fromStatusCode(400)); + lookUpMap.put( "unknown-element", Status.fromStatusCode(400)); + lookUpMap.put( "unknown-namespace", Status.fromStatusCode(400)); + lookUpMap.put( "access-denied", Status.fromStatusCode(403)); + lookUpMap.put( "lock-denied", Status.fromStatusCode(409)); + lookUpMap.put( "resource-denied", Status.fromStatusCode(409)); + lookUpMap.put( "rollback-failed", Status.fromStatusCode(500)); + lookUpMap.put( "data-exists", Status.fromStatusCode(409)); + lookUpMap.put( "data-missing", Status.fromStatusCode(409)); + lookUpMap.put( "operation-not-supported", Status.fromStatusCode(501)); + lookUpMap.put( "operation-failed", Status.fromStatusCode(500)); + lookUpMap.put( "partial-operation", Status.fromStatusCode(500)); + lookUpMap.put( "malformed-message", Status.fromStatusCode(400)); + + for( ErrorTag tag : ErrorTag.values() ) + { + Status expectedStatusCode = lookUpMap.get( tag.getTagValue() ); + assertNotNull( "Failed to find " + tag.getTagValue(), expectedStatusCode ); + assertEquals( "Status Code does not match", expectedStatusCode, tag.getStatusCode() ); + } + } + + @Test + public void testRestConfDocumentedException_NoCause() + { + String expectedMessage = "Message"; + ErrorType expectedErrorType = ErrorType.RPC; + ErrorTag expectedErrorTag = ErrorTag.IN_USE; + RestconfError e = + new RestconfError( expectedErrorType, + expectedErrorTag, expectedMessage ); + + validateRestConfError(expectedMessage, expectedErrorType, expectedErrorTag, + null, (String)null, e); + } + + @Test + public void testRestConfDocumentedException_WithAppTag() + { + String expectedMessage = "Message"; + ErrorType expectedErrorType = ErrorType.RPC; + ErrorTag expectedErrorTag = ErrorTag.IN_USE; + String expectedErrorAppTag = "application.tag"; + + RestconfError e = + new RestconfError( expectedErrorType, + expectedErrorTag, expectedMessage, expectedErrorAppTag ); + + validateRestConfError(expectedMessage, expectedErrorType, expectedErrorTag, + expectedErrorAppTag, (String)null, e); + } + + @Test + public void testRestConfDocumentedException_WithAppTagErrorInfo() + { + String expectedMessage = "Message"; + ErrorType expectedErrorType = ErrorType.RPC; + ErrorTag expectedErrorTag = ErrorTag.IN_USE; + String expectedErrorAppTag = "application.tag"; + String errorInfo = "session.id"; + + RestconfError e = new RestconfError( expectedErrorType, + expectedErrorTag, + expectedMessage, + expectedErrorAppTag, + errorInfo ); + + validateRestConfError(expectedMessage, expectedErrorType, expectedErrorTag, + expectedErrorAppTag, errorInfo, e); + } + + @Test + public void testRestConfErrorWithRpcError() { + + // All fields set + RpcError rpcError = getRpcError( "mock app-tag", ErrorTag.BAD_ATTRIBUTE.getTagValue(), + "mock error-info", RpcError.ErrorSeverity.ERROR, + "mock error-message", RpcError.ErrorType.PROTOCOL, + new Exception( "mock cause" ) ); + + validateRestConfError( "mock error-message", ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE, + "mock app-tag", "mock error-info", + new RestconfError( rpcError ) ); + + // All fields set except 'info' - expect error-info set to 'cause' + rpcError = getRpcError( "mock app-tag", ErrorTag.BAD_ATTRIBUTE.getTagValue(), + null, RpcError.ErrorSeverity.ERROR, + "mock error-message", RpcError.ErrorType.PROTOCOL, + new Exception( "mock cause" ) ); + + validateRestConfError( "mock error-message", ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE, + "mock app-tag", new Contains( "mock cause" ), + new RestconfError( rpcError ) ); + + // Some fields set - expect error-info set to ErrorSeverity + rpcError = getRpcError( null, ErrorTag.ACCESS_DENIED.getTagValue(), + null, RpcError.ErrorSeverity.ERROR, + null, RpcError.ErrorType.RPC, null ); + + validateRestConfError( null, ErrorType.RPC, ErrorTag.ACCESS_DENIED, + null, "error", + new RestconfError( rpcError ) ); + + // 'tag' field not mapped to ErrorTag - expect error-tag set to OPERATION_FAILED + rpcError = getRpcError( null, "not mapped", + null, RpcError.ErrorSeverity.WARNING, + null, RpcError.ErrorType.TRANSPORT, null ); + + validateRestConfError( null, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED, + null, "warning", + new RestconfError( rpcError ) ); + + // No fields set - edge case + rpcError = getRpcError( null, null, null, null, null, null, null ); + + validateRestConfError( null, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, + null, (String)null, new RestconfError( rpcError ) ); + } + + private void validateRestConfError(String expectedMessage, ErrorType expectedErrorType, + ErrorTag expectedErrorTag, String expectedErrorAppTag, String errorInfo, RestconfError e) { + + validateRestConfError( expectedMessage, expectedErrorType, expectedErrorTag, + expectedErrorAppTag, equalTo( errorInfo ), e ); + } + + private void validateRestConfError(String expectedMessage, ErrorType expectedErrorType, + ErrorTag expectedErrorTag, String expectedErrorAppTag, + Matcher errorInfoMatcher, RestconfError e) { + + assertEquals( "getErrorMessage", expectedMessage, e.getErrorMessage() ); + assertEquals( "getErrorType", expectedErrorType, e.getErrorType() ); + assertEquals( "getErrorTag", expectedErrorTag, e.getErrorTag() ); + assertEquals( "getErrorAppTag", expectedErrorAppTag, e.getErrorAppTag() ); + assertThat( "getErrorInfo", e.getErrorInfo(), errorInfoMatcher ); + e.toString(); // really just checking for NPE etc. Don't care about contents. + } +} diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/URITest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/URITest.java index eef9e414e9..33d4b325be 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/URITest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/URITest.java @@ -26,7 +26,7 @@ import org.opendaylight.controller.sal.core.api.mount.MountService; import org.opendaylight.controller.sal.restconf.impl.BrokerFacade; import org.opendaylight.controller.sal.restconf.impl.ControllerContext; import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode; -import org.opendaylight.controller.sal.restconf.impl.ResponseException; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.controller.sal.restconf.impl.RestconfImpl; import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier; import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; @@ -67,15 +67,13 @@ public class URITest { @Test public void testToInstanceIdentifierListWithNullKey() { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); + exception.expect(RestconfDocumentedException.class); controllerContext.toInstanceIdentifier("simple-nodes:user/null/boo"); } @Test public void testToInstanceIdentifierListWithMissingKey() { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); + exception.expect(RestconfDocumentedException.class); controllerContext.toInstanceIdentifier("simple-nodes:user/foo"); } @@ -96,29 +94,25 @@ public class URITest { @Test public void testToInstanceIdentifierChoiceException() { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); + exception.expect(RestconfDocumentedException.class); controllerContext.toInstanceIdentifier("simple-nodes:food/snack"); } @Test public void testToInstanceIdentifierCaseException() { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); + exception.expect(RestconfDocumentedException.class); controllerContext.toInstanceIdentifier("simple-nodes:food/sports-arena"); } @Test public void testToInstanceIdentifierChoiceCaseException() { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); + exception.expect(RestconfDocumentedException.class); controllerContext.toInstanceIdentifier("simple-nodes:food/snack/sports-arena"); } - + @Test public void testToInstanceIdentifierWithoutNode() { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); + exception.expect(RestconfDocumentedException.class); controllerContext.toInstanceIdentifier("simple-nodes"); } @@ -142,24 +136,22 @@ public class URITest { @Test public void testMountPointWithoutMountService() throws FileNotFoundException { - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 503 Service Unavailable"); - + exception.expect(RestconfDocumentedException.class); + controllerContext.setMountService(null); InstanceIdWithSchemaNode instanceIdentifier = controllerContext .toInstanceIdentifier("simple-nodes:users/yang-ext:mount/test-interface2:class/student/name"); } - + @Test public void testMountPointWithoutMountPointSchema() { initMountService(false); - exception.expect(ResponseException.class); - exception.expectMessage("HTTP 400 Bad Request"); - + exception.expect(RestconfDocumentedException.class); + InstanceIdWithSchemaNode instanceIdentifier = controllerContext .toInstanceIdentifier("simple-nodes:users/yang-ext:mount/test-interface2:class"); } - + public void initMountService(boolean withSchema) { MountService mountService = mock(MountService.class); controllerContext.setMountService(mountService); diff --git a/opendaylight/md-sal/samples/toaster-provider/src/main/java/org/opendaylight/controller/sample/toaster/provider/OpendaylightToaster.java b/opendaylight/md-sal/samples/toaster-provider/src/main/java/org/opendaylight/controller/sample/toaster/provider/OpendaylightToaster.java index b4da5a3d22..2ecd7e7b68 100644 --- a/opendaylight/md-sal/samples/toaster-provider/src/main/java/org/opendaylight/controller/sample/toaster/provider/OpendaylightToaster.java +++ b/opendaylight/md-sal/samples/toaster-provider/src/main/java/org/opendaylight/controller/sample/toaster/provider/OpendaylightToaster.java @@ -37,7 +37,9 @@ import org.opendaylight.controller.sal.binding.api.data.DataChangeListener; import org.opendaylight.yangtools.yang.binding.DataObject; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; import org.opendaylight.yangtools.yang.common.RpcError; +import org.opendaylight.yangtools.yang.common.RpcError.ErrorType; import org.opendaylight.yangtools.yang.common.RpcResult; +import org.opendaylight.yangtools.yang.common.RpcError.ErrorSeverity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -157,14 +159,15 @@ public class OpendaylightToaster implements ToasterService, ToasterProviderRunti LOG.info( "Toaster is already making toast" ); RpcResult result = Rpcs. getRpcResult(false, null, Arrays.asList( - RpcErrors.getRpcError( null, null, null, null, - "Toaster is busy", null, null ) ) ); + RpcErrors.getRpcError( "", "in-use", null, ErrorSeverity.WARNING, + "Toaster is busy", ErrorType.APPLICATION, null ) ) ); return Futures.immediateFuture(result); } else if( outOfBread() ) { RpcResult result = Rpcs. getRpcResult(false, null, Arrays.asList( - RpcErrors.getRpcError( null, null, null, null, - "Toaster is out of bread", null, null ) ) ); + RpcErrors.getRpcError( "out-of-stock", "resource-denied", null, null, + "Toaster is out of bread", + ErrorType.APPLICATION, null ) ) ); return Futures.immediateFuture(result); } else { -- 2.36.6