From: tpantelis Date: Wed, 4 Jun 2014 22:53:36 +0000 (-0400) Subject: Bug 225 - added support for depth RESTCONF parameter in URI X-Git-Tag: release/helium~645^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=0c3deecf9e9f8e5eb2c099c784f417f938134c89 Bug 225 - added support for depth RESTCONF parameter in URI Modified RestconfImpl to prune the resulting ComositeNode tree based on the depth parameter, if specified. A depth of 1 retrieves just the target resource and no children. This is a way to determine if the resource exists. A depth of 2 retrieves the target resource and just its direct children - any child container would be empty. Change-Id: I44d99f9886e3837a1bb13eda84aec3c4cc6b21ae Signed-off-by: tpantelis --- diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/RestconfService.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/RestconfService.java index 4d9b198795..056be72d4e 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/RestconfService.java +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/RestconfService.java @@ -107,13 +107,15 @@ public interface RestconfService { @Path("/config/{identifier:.+}") @Produces({Draft02.MediaTypes.DATA+JSON,Draft02.MediaTypes.DATA+XML, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML}) - public StructuredData readConfigurationData(@Encoded @PathParam("identifier") String identifier); + public StructuredData readConfigurationData(@Encoded @PathParam("identifier") String identifier, + @Context UriInfo depth); @GET @Path("/operational/{identifier:.+}") @Produces({Draft02.MediaTypes.DATA+JSON,Draft02.MediaTypes.DATA+XML, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_XML}) - public StructuredData readOperationalData(@Encoded @PathParam("identifier") String identifier); + public StructuredData readOperationalData(@Encoded @PathParam("identifier") String identifier, + @Context UriInfo depth); @PUT @Path("/config/{identifier:.+}") 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 c0ce90e15d..7b6dcd57db 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 @@ -49,6 +49,7 @@ import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.InstanceIdent import org.opendaylight.yangtools.yang.data.api.MutableCompositeNode; import org.opendaylight.yangtools.yang.data.api.Node; import org.opendaylight.yangtools.yang.data.api.SimpleNode; +import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode; import org.opendaylight.yangtools.yang.data.impl.NodeFactory; import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; @@ -71,6 +72,7 @@ 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.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -566,7 +568,7 @@ public class RestconfImpl implements RestconfService { } @Override - public StructuredData readConfigurationData(final String identifier) { + public StructuredData readConfigurationData(final String identifier, UriInfo info) { final InstanceIdWithSchemaNode iiWithData = this.controllerContext.toInstanceIdentifier(identifier); CompositeNode data = null; MountInstance mountPoint = iiWithData.getMountPoint(); @@ -577,11 +579,57 @@ public class RestconfImpl implements RestconfService { data = broker.readConfigurationData(iiWithData.getInstanceIdentifier()); } + data = pruneDataAtDepth( data, parseDepthParameter( info ) ); return new StructuredData(data, iiWithData.getSchemaNode(), iiWithData.getMountPoint()); } + @SuppressWarnings("unchecked") + private > T pruneDataAtDepth( T node, Integer depth ) { + if( depth == null ) { + return node; + } + + if( node instanceof CompositeNode ) { + ImmutableList.Builder> newChildNodes = ImmutableList.> builder(); + if( depth > 1 ) { + for( Node childNode: ((CompositeNode)node).getValue() ) { + newChildNodes.add( pruneDataAtDepth( childNode, depth - 1 ) ); + } + } + + return (T) ImmutableCompositeNode.create( node.getNodeType(), newChildNodes.build() ); + } + else { // SimpleNode + return node; + } + } + + private Integer parseDepthParameter( UriInfo info ) { + String param = info.getQueryParameters( false ).getFirst( "depth" ); + if( Strings.isNullOrEmpty( param ) || "unbounded".equals( param ) ) { + return null; + } + + try { + Integer depth = Integer.valueOf( param ); + if( depth < 1 ) { + throw new RestconfDocumentedException( new RestconfError( + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, "Invalid depth parameter: " + depth, + null, "The depth parameter must be an integer > 1 or \"unbounded\"" ) ); + } + + return depth; + } + catch( NumberFormatException e ) { + throw new RestconfDocumentedException( new RestconfError( + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, + "Invalid depth parameter: " + e.getMessage(), + null, "The depth parameter must be an integer > 1 or \"unbounded\"" ) ); + } + } + @Override - public StructuredData readOperationalData(final String identifier) { + public StructuredData readOperationalData(final String identifier, UriInfo info) { final InstanceIdWithSchemaNode iiWithData = this.controllerContext.toInstanceIdentifier(identifier); CompositeNode data = null; MountInstance mountPoint = iiWithData.getMountPoint(); @@ -592,6 +640,7 @@ public class RestconfImpl implements RestconfService { data = broker.readOperationalData(iiWithData.getInstanceIdentifier()); } + data = pruneDataAtDepth( data, parseDepthParameter( info ) ); return new StructuredData(data, iiWithData.getSchemaNode(), mountPoint); } diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/MediaTypesTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/MediaTypesTest.java index 2037fd4862..319603dfc1 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/MediaTypesTest.java +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/MediaTypesTest.java @@ -23,6 +23,7 @@ import java.io.UnsupportedEncodingException; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Application; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; @@ -96,21 +97,21 @@ public class MediaTypesTest extends JerseyTest { String uriPrefix = "/config/"; String uriPath = "ietf-interfaces:interfaces"; String uri = uriPrefix + uriPath; - when(restconfService.readConfigurationData(uriPath)).thenReturn(null); + when(restconfService.readConfigurationData(eq(uriPath), any(UriInfo.class))).thenReturn(null); get(uri, Draft02.MediaTypes.DATA+JSON); - verify(restconfService, times(1)).readConfigurationData(uriPath); + verify(restconfService, times(1)).readConfigurationData(eq(uriPath), any(UriInfo.class)); get(uri, Draft02.MediaTypes.DATA+XML); - verify(restconfService, times(2)).readConfigurationData(uriPath); + verify(restconfService, times(2)).readConfigurationData(eq(uriPath), any(UriInfo.class)); get(uri, MediaType.APPLICATION_JSON); - verify(restconfService, times(3)).readConfigurationData(uriPath); + verify(restconfService, times(3)).readConfigurationData(eq(uriPath), any(UriInfo.class)); get(uri, MediaType.APPLICATION_XML); - verify(restconfService, times(4)).readConfigurationData(uriPath); + verify(restconfService, times(4)).readConfigurationData(eq(uriPath), any(UriInfo.class)); get(uri, MediaType.TEXT_XML); - verify(restconfService, times(5)).readConfigurationData(uriPath); + verify(restconfService, times(5)).readConfigurationData(eq(uriPath), any(UriInfo.class)); // negative tests get(uri, MediaType.TEXT_PLAIN); - verify(restconfService, times(5)).readConfigurationData(uriPath); + verify(restconfService, times(5)).readConfigurationData(eq(uriPath), any(UriInfo.class)); } @Test @@ -118,21 +119,21 @@ public class MediaTypesTest extends JerseyTest { String uriPrefix = "/operational/"; String uriPath = "ietf-interfaces:interfaces"; String uri = uriPrefix + uriPath; - when(restconfService.readOperationalData(uriPath)).thenReturn(null); + when(restconfService.readOperationalData(eq(uriPath), any(UriInfo.class))).thenReturn(null); get(uri, Draft02.MediaTypes.DATA+JSON); - verify(restconfService, times(1)).readOperationalData(uriPath); + verify(restconfService, times(1)).readOperationalData(eq(uriPath), any(UriInfo.class)); get(uri, Draft02.MediaTypes.DATA+XML); - verify(restconfService, times(2)).readOperationalData(uriPath); + verify(restconfService, times(2)).readOperationalData(eq(uriPath), any(UriInfo.class)); get(uri, MediaType.APPLICATION_JSON); - verify(restconfService, times(3)).readOperationalData(uriPath); + verify(restconfService, times(3)).readOperationalData(eq(uriPath), any(UriInfo.class)); get(uri, MediaType.APPLICATION_XML); - verify(restconfService, times(4)).readOperationalData(uriPath); + verify(restconfService, times(4)).readOperationalData(eq(uriPath), any(UriInfo.class)); get(uri, MediaType.TEXT_XML); - verify(restconfService, times(5)).readOperationalData(uriPath); + verify(restconfService, times(5)).readOperationalData(eq(uriPath), any(UriInfo.class)); // negative tests get(uri, MediaType.TEXT_PLAIN); - verify(restconfService, times(5)).readOperationalData(uriPath); + verify(restconfService, times(5)).readOperationalData(eq(uriPath), any(UriInfo.class)); } @Test 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 893622f60a..f0a232fba6 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 @@ -7,15 +7,17 @@ */ package org.opendaylight.controller.sal.restconf.impl.test; -import static junit.framework.Assert.assertNotNull; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.FileNotFoundException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; @@ -24,17 +26,23 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.core.Application; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; 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; @@ -45,6 +53,7 @@ import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider; 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.RestconfDocumentedException; import org.opendaylight.controller.sal.restconf.impl.RestconfImpl; import org.opendaylight.controller.sal.restconf.impl.SimpleNodeWrapper; import org.opendaylight.yangtools.yang.common.QName; @@ -52,10 +61,28 @@ import org.opendaylight.yangtools.yang.data.api.CompositeNode; import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument; import org.opendaylight.yangtools.yang.data.api.Node; +import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode; +import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder; import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; public class RestGetOperationTest extends JerseyTest { + static class NodeData { + Object key; + Object data; // List for a CompositeNode, value Object for a SimpleNode + + NodeData( Object key, Object data ) { + this.key = key; + this.data = data; + } + } + private static BrokerFacade brokerFacade; private static RestconfImpl restconfImpl; private static SchemaContext schemaContextYangsIetf; @@ -588,4 +615,314 @@ public class RestGetOperationTest extends JerseyTest { return null; } + @Test + public void getDataWithUriDepthParameterTest() throws UnsupportedEncodingException { + + ControllerContext.getInstance().setGlobalSchema( schemaContextModules ); + + CompositeNode depth1Cont = toCompositeNode( + toCompositeNodeData( toNestedQName( "depth1-cont" ), + toCompositeNodeData( toNestedQName( "depth2-cont1" ), + toCompositeNodeData( toNestedQName( "depth3-cont1" ), + toCompositeNodeData( toNestedQName( "depth4-cont1" ), + toSimpleNodeData( toNestedQName( "depth5-leaf1" ), "depth5-leaf1-value" ) + ), + toSimpleNodeData( toNestedQName( "depth4-leaf1" ), "depth4-leaf1-value" ) + ), + toSimpleNodeData( toNestedQName( "depth3-leaf1" ), "depth3-leaf1-value" ) + ), + toCompositeNodeData( toNestedQName( "depth2-cont2" ), + toCompositeNodeData( toNestedQName( "depth3-cont2" ), + toCompositeNodeData( toNestedQName( "depth4-cont2" ), + toSimpleNodeData( toNestedQName( "depth5-leaf2" ), "depth5-leaf2-value" ) + ), + toSimpleNodeData( toNestedQName( "depth4-leaf2" ), "depth4-leaf2-value" ) + ), + toSimpleNodeData( toNestedQName( "depth3-leaf2" ), "depth3-leaf2-value" ) + ), + toSimpleNodeData( toNestedQName( "depth2-leaf1" ), "depth2-leaf1-value" ) + ) ); + + when( brokerFacade.readConfigurationData( any( InstanceIdentifier.class ) ) ) + .thenReturn( depth1Cont ); + + // Test config with depth 1 + + Response response = target( "/config/nested-module:depth1-cont" ).queryParam( "depth", "1" ) + .request( "application/xml" ).get(); + + verifyXMLResponse( response, expectEmptyContainer( "depth1-cont" ) ); + + // Test config with depth 2 + + response = target( "/config/nested-module:depth1-cont" ).queryParam( "depth", "2" ) + .request( "application/xml" ).get(); + +// String xml="depth2-leaf1-value"; +// Response mr=mock(Response.class); +// when(mr.getEntity()).thenReturn( new java.io.StringBufferInputStream(xml) ); + + verifyXMLResponse( response, + expectContainer( "depth1-cont", + expectEmptyContainer( "depth2-cont1" ), + expectEmptyContainer( "depth2-cont2" ), + expectLeaf( "depth2-leaf1", "depth2-leaf1-value" ) + ) ); + + // Test config with depth 3 + + response = target( "/config/nested-module:depth1-cont" ).queryParam( "depth", "3" ) + .request( "application/xml" ).get(); + + verifyXMLResponse( response, + expectContainer( "depth1-cont", + expectContainer( "depth2-cont1", + expectEmptyContainer( "depth3-cont1" ), + expectLeaf( "depth3-leaf1", "depth3-leaf1-value" ) + ), + expectContainer( "depth2-cont2", + expectEmptyContainer( "depth3-cont2" ), + expectLeaf( "depth3-leaf2", "depth3-leaf2-value" ) + ), + expectLeaf( "depth2-leaf1", "depth2-leaf1-value" ) + ) ); + + // Test config with depth 4 + + response = target( "/config/nested-module:depth1-cont" ).queryParam( "depth", "4" ) + .request( "application/xml" ).get(); + + verifyXMLResponse( response, + expectContainer( "depth1-cont", + expectContainer( "depth2-cont1", + expectContainer( "depth3-cont1", + expectEmptyContainer( "depth4-cont1" ), + expectLeaf( "depth4-leaf1", "depth4-leaf1-value" ) + ), + expectLeaf( "depth3-leaf1", "depth3-leaf1-value" ) + ), + expectContainer( "depth2-cont2", + expectContainer( "depth3-cont2", + expectEmptyContainer( "depth4-cont2" ), + expectLeaf( "depth4-leaf2", "depth4-leaf2-value" ) + ), + expectLeaf( "depth3-leaf2", "depth3-leaf2-value" ) + ), + expectLeaf( "depth2-leaf1", "depth2-leaf1-value" ) + ) ); + + // Test config with depth 5 + + response = target( "/config/nested-module:depth1-cont" ).queryParam( "depth", "5" ) + .request( "application/xml" ).get(); + + verifyXMLResponse( response, + expectContainer( "depth1-cont", + expectContainer( "depth2-cont1", + expectContainer( "depth3-cont1", + expectContainer( "depth4-cont1", + expectLeaf( "depth5-leaf1", "depth5-leaf1-value" ) + ), + expectLeaf( "depth4-leaf1", "depth4-leaf1-value" ) + ), + expectLeaf( "depth3-leaf1", "depth3-leaf1-value" ) + ), + expectContainer( "depth2-cont2", + expectContainer( "depth3-cont2", + expectContainer( "depth4-cont2", + expectLeaf( "depth5-leaf2", "depth5-leaf2-value" ) + ), + expectLeaf( "depth4-leaf2", "depth4-leaf2-value" ) + ), + expectLeaf( "depth3-leaf2", "depth3-leaf2-value" ) + ), + expectLeaf( "depth2-leaf1", "depth2-leaf1-value" ) + ) ); + + // Test config with depth unbounded + + response = target( "/config/nested-module:depth1-cont" ).queryParam( "depth", "unbounded" ) + .request( "application/xml" ).get(); + + verifyXMLResponse( response, + expectContainer( "depth1-cont", + expectContainer( "depth2-cont1", + expectContainer( "depth3-cont1", + expectContainer( "depth4-cont1", + expectLeaf( "depth5-leaf1", "depth5-leaf1-value" ) + ), + expectLeaf( "depth4-leaf1", "depth4-leaf1-value" ) + ), + expectLeaf( "depth3-leaf1", "depth3-leaf1-value" ) + ), + expectContainer( "depth2-cont2", + expectContainer( "depth3-cont2", + expectContainer( "depth4-cont2", + expectLeaf( "depth5-leaf2", "depth5-leaf2-value" ) + ), + expectLeaf( "depth4-leaf2", "depth4-leaf2-value" ) + ), + expectLeaf( "depth3-leaf2", "depth3-leaf2-value" ) + ), + expectLeaf( "depth2-leaf1", "depth2-leaf1-value" ) + ) ); + + // Test operational + + CompositeNode depth2Cont1 = toCompositeNode( + toCompositeNodeData( toNestedQName( "depth2-cont1" ), + toCompositeNodeData( toNestedQName( "depth3-cont1" ), + toCompositeNodeData( toNestedQName( "depth4-cont1" ), + toSimpleNodeData( toNestedQName( "depth5-leaf1" ), "depth5-leaf1-value" ) + ), + toSimpleNodeData( toNestedQName( "depth4-leaf1" ), "depth4-leaf1-value" ) + ), + toSimpleNodeData( toNestedQName( "depth3-leaf1" ), "depth3-leaf1-value" ) + ) ); + + when( brokerFacade.readOperationalData( any( InstanceIdentifier.class ) ) ) + .thenReturn( depth2Cont1 ); + + response = target( "/operational/nested-module:depth1-cont/depth2-cont1" ) + .queryParam( "depth", "3" ).request( "application/xml" ).get(); + + verifyXMLResponse( response, + expectContainer( "depth2-cont1", + expectContainer( "depth3-cont1", + expectEmptyContainer( "depth4-cont1" ), + expectLeaf( "depth4-leaf1", "depth4-leaf1-value" ) + ), + expectLeaf( "depth3-leaf1", "depth3-leaf1-value" ) + ) ); + } + + @Test + public void getDataWithInvalidDepthParameterTest() { + + ControllerContext.getInstance().setGlobalSchema( schemaContextModules ); + + final MultivaluedMap paramMap = new MultivaluedHashMap<>(); + paramMap.putSingle( "depth", "1o" ); + UriInfo mockInfo = mock( UriInfo.class ); + when( mockInfo.getQueryParameters( false ) ).thenAnswer( + new Answer>() { + @Override + public MultivaluedMap answer( InvocationOnMock invocation ) { + return paramMap; + } + } ); + + getDataWithInvalidDepthParameterTest( mockInfo ); + + paramMap.putSingle( "depth", "0" ); + getDataWithInvalidDepthParameterTest( mockInfo ); + + paramMap.putSingle( "depth", "-1" ); + getDataWithInvalidDepthParameterTest( mockInfo ); + } + + private void getDataWithInvalidDepthParameterTest( UriInfo uriInfo ) { + try { + restconfImpl.readConfigurationData( "nested-module:depth1-cont", uriInfo ); + fail( "Expected RestconfDocumentedException" ); + } + catch( RestconfDocumentedException e ) { + assertTrue( "Unexpected error message: " + e.getErrors().get( 0 ).getErrorMessage(), + e.getErrors().get( 0 ).getErrorMessage().contains( "depth" ) ); + } + } + + private void verifyXMLResponse( Response response, NodeData nodeData ) { + + Document doc = TestUtils.loadDocumentFrom( (InputStream) response.getEntity() ); + assertNotNull( "Could not parse XML document", doc ); + + //System.out.println(TestUtils.getDocumentInPrintableForm( doc )); + + verifyContainerElement( doc.getDocumentElement(), nodeData ); + } + + @SuppressWarnings("unchecked") + private void verifyContainerElement( Element element, NodeData nodeData ) { + + assertEquals( "Element local name", nodeData.key, element.getNodeName() ); + + NodeList childNodes = element.getChildNodes(); + if( nodeData.data == null ) { // empty container + assertTrue( "Expected no child elements for \"" + element.getNodeName() + "\"", + childNodes.getLength() == 0 ); + return; + } + + Map expChildMap = Maps.newHashMap(); + for( NodeData expChild: (List)nodeData.data ) { + expChildMap.put( expChild.key.toString(), expChild ); + } + + for( int i = 0; i < childNodes.getLength(); i++ ) { + org.w3c.dom.Node actualChild = childNodes.item( i ); + if( !( actualChild instanceof Element ) ) { + continue; + } + + Element actualElement = (Element)actualChild; + NodeData expChild = expChildMap.remove( actualElement.getNodeName() ); + assertNotNull( "Unexpected child element for parent \"" + element.getNodeName() + + "\": " + actualElement.getNodeName(), expChild ); + + if( expChild.data == null || expChild.data instanceof List ) { + verifyContainerElement( actualElement, expChild ); + } + else { + assertEquals( "Text content for element: " + actualElement.getNodeName(), + expChild.data, actualElement.getTextContent() ); + } + } + + if( !expChildMap.isEmpty() ) { + fail( "Missing elements for parent \"" + element.getNodeName() + + "\": " + expChildMap.keySet() ); + } + } + + private NodeData expectContainer( String name, NodeData... childData ) { + return new NodeData( name, Lists.newArrayList( childData ) ); + } + + private NodeData expectEmptyContainer( String name ) { + return new NodeData( name, null ); + } + + private NodeData expectLeaf( String name, Object value ) { + return new NodeData( name, value ); + } + + private QName toNestedQName( String localName ) { + return QName.create( "urn:nested:module", "2014-06-3", localName ); + } + + @SuppressWarnings("unchecked") + private CompositeNode toCompositeNode( NodeData nodeData ) { + CompositeNodeBuilder builder = ImmutableCompositeNode.builder(); + builder.setQName( (QName) nodeData.key ); + + for( NodeData child: (List)nodeData.data ) { + if( child.data instanceof List ) { + builder.add( toCompositeNode( child ) ); + } + else { + builder.addLeaf( (QName) child.key, child.data ); + } + } + + return builder.toInstance(); + } + + private NodeData toCompositeNodeData( QName key, NodeData... childData ) { + return new NodeData( key, Lists.newArrayList( childData ) ); + } + + private NodeData toSimpleNodeData( QName key, Object value ) { + return new NodeData( key, value ); + } } 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 index 3f984c293b..e146cf8f4f 100644 --- 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 @@ -34,6 +34,7 @@ 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.ws.rs.core.UriInfo; import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; @@ -220,7 +221,7 @@ public class RestconfDocumentedExceptionMapperTest extends JerseyTest { void stageMockEx( final RestconfDocumentedException ex ) { reset( mockRestConf ); - when( mockRestConf.readOperationalData( any( String.class ) ) ).thenThrow( ex ); + when( mockRestConf.readOperationalData( any( String.class ), any( UriInfo.class ) ) ).thenThrow( ex ); } void testJsonResponse( final RestconfDocumentedException ex, final Status expStatus, final ErrorType expErrorType, @@ -776,8 +777,8 @@ public class RestconfDocumentedExceptionMapperTest extends JerseyTest { // The StructuredDataToJsonProvider should throw a RestconfDocumentedException with no data - when( mockRestConf.readOperationalData( any( String.class ) ) ) - .thenReturn( new StructuredData( null, null, null ) ); + when( mockRestConf.readOperationalData( any( String.class ), any( UriInfo.class ) ) ) + .thenReturn( new StructuredData( null, null, null ) ); Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/resources/modules/nested-module.yang b/opendaylight/md-sal/sal-rest-connector/src/test/resources/modules/nested-module.yang new file mode 100644 index 0000000000..590743c9ca --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/resources/modules/nested-module.yang @@ -0,0 +1,47 @@ +module nested-module { + namespace "urn:nested:module"; + prefix "nested"; + revision "2014-06-3"; + + container depth1-cont { + container depth2-cont1 { + container depth3-cont1 { + container depth4-cont1 { + leaf depth5-leaf1 { + type string; + } + } + + leaf depth4-leaf1 { + type string; + } + } + + leaf depth3-leaf1 { + type string; + } + } + + container depth2-cont2 { + container depth3-cont2 { + container depth4-cont2 { + leaf depth5-leaf2 { + type string; + } + } + + leaf depth4-leaf2 { + type string; + } + } + + leaf depth3-leaf2 { + type string; + } + } + + leaf depth2-leaf1 { + type string; + } + } +} \ No newline at end of file