Filtering Data with Binding Query, using OpenDaylight

[OpenDaylight] Binding Query

Binding Query (BQ) is an MD-SAL module, currently located at OpenDaylight version master (7.0.5), 6.0.x, and 5.0.x. Its primary function is to filter data from the Binding Awareness model.

To use BQ, it is required to create QueryExpression and QueryExecutor. QueryExecutor contains BindingCodecTree and data represented by the Binding Awareness model. A filter will be applied to this data and all operations from QueryExpression.

QueryExpression is created from the QueryFactory class. Creating a QueryFactory is started with the querySubtree method. Here, it is entered as an instance identifier, and it has to be at the root of data from QueryExecutor.

The next step will be to create a path to the data, which we want to filter – and then apply the required filter. When QueryExpression is ready, it will be applied with the method executeQuery in QueryExecutor. One QueryExpression can be used on multiple QueryExecutors with the same data schema.

Prerequisites for Binding Query

Now, we will demonstrate how to actually use Binding Query. We will create a YANG model for this purpose:

module queryTest {
    yang-version 1.1;
    namespace urn:yang.query;
    prefix qt;
 
    revision 2021-01-20 {
        description
          "Initial revision";
    }
 
    grouping container-root {
        container container-root {
            leaf root-leaf {
                type string;
            }
 
            leaf-list root-leaf-list {
                type string;
            }
 
            container container-nested {
                leaf nested-leaf {
                    type uint32;
                }
            }
        }
    }
 
    grouping list-root {
        container list-root {
            list top-list {
                key "key-a key-b";
 
                leaf key-a {
                    type string;
                }
                leaf key-b {
                    type string;
                }
                list nested-list {
                    key "identifier";
 
                    leaf identifier {
                        type string;
                    }
 
                    leaf weight {
                        type int16;
                    }
                }
            }
        }
    }
 
    grouping choice {
        choice choice {
            case case-a {
                container case-a-container {
                    leaf case-a-leaf {
                        type int32;
                    }
                }
            }
            case case-b {
                list case-b-container {
                    key "key-cb";
                    leaf key-cb {
                        type string;
                    }
                }
            }
        }
    }
 
    container root {
        uses container-root;
        uses list-root;
        uses choice;
    }
}

Then, we will build and create a Binding Awareness model, with some test data from the provided YANG model.

public Root generateQueryData() {
    HashMap<NestedListKey, NestedList> nestedMap = new HashMap<>() {{
        put(new NestedListKey("NestedId"), new NestedListBuilder()
            .setIdentifier("NestedId")
            .setWeight((short) 10)
            .build());
        put(new NestedListKey("NestedId2"), new NestedListBuilder()
            .setIdentifier("NestedId2")
            .setWeight((short) 15)
            .build());
    }};

    HashMap<NestedListKey, NestedList> nestedMap2 = new HashMap<>() {{
        put(new NestedListKey("Nested2Id"), new NestedListBuilder()
            .setIdentifier("Nested2Id")
            .setWeight((short) 10)
            .build());
    }};

    HashMap<TopListKey, TopList> topMap = new HashMap<>() {{
        put(new TopListKey("keyA", "keyB"),
            new TopListBuilder()
                .setKeyA("keyA")
                .setKeyB("keyB")
                .setNestedList(nestedMap)
                .build());
        put(new TopListKey("keyA2", "keyB2"),
            new TopListBuilder()
                .setKeyA("keyA2")
                .setKeyB("keyB2")
                .setNestedList(nestedMap2)
                .build());
    }};

    HashMap<CaseBContainerKey, CaseBContainer> caseBMap = new HashMap<>() {{
        put(new CaseBContainerKey("test@test.com"),
            new CaseBContainerBuilder()
                .setKeyCb("test@test.com")
                .build());
        put(new CaseBContainerKey("test"),
            new CaseBContainerBuilder()
                .setKeyCb("test")
                .build());
    }};

    RootBuilder rootBuilder = new RootBuilder();
    rootBuilder.setContainerRoot(new ContainerRootBuilder()
                                     .setRootLeaf("root leaf")
                                     .setContainerNested(new ContainerNestedBuilder()
                                                             .setNestedLeaf(Uint32.valueOf(10))
                                                             .build())
                                     .setRootLeafList(new ArrayList<>() {{
                                         add("data1");
                                         add("data2");
                                         add("data3");
                                     }})
                                     .build());
    rootBuilder.setListRoot(new ListRootBuilder().setTopList(topMap).build());
    rootBuilder.setChoiceRoot(new CaseBBuilder()
                                  .setCaseBContainer(caseBMap)
                                  .build());
    return rootBuilder.build();
}

For better orientation in the test-data structure, there is also a JSON representation of the data we will use:

{
  "queryTest:root": {
    "container-root": {
      "root-leaf": "root leaf",
      "root-leaf-list": [
        "data1",
        "data2",
        "data3"
      ],
      "container-nested": {
        "nested-leaf": 10
      }
    },
    "list-root": {
      "top-list": [
        {
          "key-a": "keyA",
          "key-b": "keyB",
          "nested-list": [
            {
              "identifier": "NestedId",
              "weight": 10
            },
            {
              "identifier": "NestedId2",
              "weight": 15
            }
          ]
        },
        {
          "key-a": "keyA2",
          "key-b": "keyB2",
          "nested-list": []
        }
      ]
    },
    "choice": {
      "case-b-container": {
        "top-list": [
          {
            "key-cb": "test@test.com"
          },
          {
            "key-cb": "test"
          }
        ]
      }
    }
  }
}

From the Binding Awareness model queryTest shown above, we can create a QueryExecutor. In this example, we will use the SimpleQueryExecutor. As a builder parameter, we entered BindingCodecTree. Afterwards, this will be added into the Binding Awareness data by method, which we created above.

public QueryExecutor createExecutor() {
    return SimpleQueryExecutor.builder(CODEC)
        .add(generateQueryData())
        .build();
}

Create a Query & Filter Data

Now, we can start with an example on how to create a query and filter some data. In the first example, we will describe how to filter the container by the value of his leaf. In the next steps, we will create a QueryExpression.

  1. First, we will create a QueryFactory from the DefaultQueryFactory. The DefaultQueryFactory constructor takes BindingCodecTree as a parameter.

    QueryFactory factory = new DefaultQueryFactory(CODEC);
  2. The next step is to create the DescendantQueryBuilder from QueryFactory. The querySubtree method takes the instance identifier as a parameter. This identifier should be a root node from our model. In this case, it is a container with the name root.
    DescendantQueryBuilder<Root> decadentQueryRootBuilder
        = factory.querySubtree(InstanceIdentifier.create(Root.class));
  3. Then we will set the path to the parent container of leaf, depending on which value we want to filter.
    DescendantQueryBuilder<ContainerRoot> decadentQueryContainerRootBuilder 
    = decadentQueryRootBuilder.extractChild(ContainerRoot.class);
  4. Now we create the StringMatchingBuilder, with the value of the leaf and name root-leaf, which we want to match.
    StringMatchBuilder<ContainerRoot> stringMatchBuilder = decadentQueryContainerRootBuilder.matching()
        .leaf(ContainerRoot::getRootLeaf);
  5. The last step is to define which values should be filtered and then build the QueryExpression. For this case, we will filter a specific leaf, with the value “root leaf”.
    QueryExpression<ContainerRoot> matchRootLeaf = stringMatchBuilder.valueEquals("root leaf").build();

     

Now, the QueryExpression can be used to filter data from QueryExecutor. For creating QueryExecutor, we use the method defined above in “test query data”.

QueryExecutor executor = createExecutor();
      QueryResult<ContainerRoot> items = executor.executeQuery(matchRootLeaf);

The entire previous example in one block will look like this:

QueryFactory factory = new DefaultQueryFactory(CODEC);
        QueryExpression<ContainerRoot> rootLeafQueryExpression = factory
            .querySubtree(InstanceIdentifier.create(Root.class))
            .extractChild(ContainerRoot.class)
            .matching()
            .leaf(ContainerRoot::getRootLeaf)
            .valueEquals("root leaf")
            .build();
        
        QueryExecutor executor = createExecutor();
        QueryResult<ContainerRoot> result = executor.executeQuery(rootLeafQueryExpression);

When we validate the result, we will find, that only one item matched our condition in the query:

assertEquals(1, result.getItems().size());
      String resultItem = result.getItems().stream()
          .map(item -> item.object().getRootLeaf())
          .findFirst()
          .orElse(null);
      assertEquals("root leaf", resultItem);

Filter Nested-List Data

The next example will show how to use Binding Query to filter data from nested-list. This example will filter nested-list items, where the weight parameter equals 10.

QueryFactory factory = new DefaultQueryFactory(CODEC);
 QueryExpression<NestedList> queryExpression = factory
     .querySubtree(InstanceIdentifier.create(Root.class))
     .extractChild(ListRoot.class)
     .extractChild(TopList.class)
     .extractChild(NestedList.class)
     .matching()
     .leaf(NestedList::getWeight)
     .valueEquals((short) 10)
     .build();

 QueryExecutor executor = createExecutor();
 QueryResult<NestedList> result = executor.executeQuery(queryExpression);
 assertEquals(2, result.getItems().size())

If we are required to filter nested-list items, but only from top-list with specific keys, then it will look like this:

QueryFactory factory = new DefaultQueryFactory(CODEC);
        QueryExpression<NestedList> queryExpression = factory
            .querySubtree(InstanceIdentifier.create(Root.class))
            .extractChild(ListRoot.class)
            .extractChild(TopList.class, new TopListKey("keyA", "keyB"))
            .extractChild(NestedList.class)
            .matching()
            .leaf(NestedList::getWeight)
            .valueEquals((short) 10)
            .build();

        QueryExecutor executor = createExecutor();
        QueryResult<NestedList> result = executor.executeQuery(queryExpression);
        assertEquals(1, result.getItems().size())

In case that we wanted to get top-list elements, but only those which contain nested-leaf items with a weight greater than, or equals to, 15. It is possible to set a match on top-list containers and then continue with a condition to nested-list. With number operations, we can execute greaterThanOrEqual, lessThanOrEqual, greaterThan, and lessThan methods.

QueryExpression<TopList> queryExpression = factory
            .querySubtree(InstanceIdentifier.create(Root.class))
            .extractChild(ListRoot.class)
            .extractChild(TopList.class)
            .matching()
            .childObject(NestedList.class)
            .leaf(NestedList::getWeight).greaterThanOrEqual((short) 15)
            .build();

        QueryExecutor executor = createExecutor();
        QueryResult<TopList> result = executor.executeQuery(queryExpression);
        assertEquals(1, result.getItems().size());

        List<TopList> topListResult = result.getItems().stream()
            .map(Item::object)
            .filter(item -> item.getKeyA().equals("keyA"))
            .filter(item -> item.getKeyB().equals("keyB"))
            .collect(Collectors.toList());
        assertEquals(1, topListResult.size());

The last example shows how to filter choice data and matching their values in key-cb leaf. Conditions that are required to meet are defined in the pattern, which matches the email address.

QueryFactory factory = new DefaultQueryFactory(CODEC);
       QueryExpression<CaseBContainer> queryExpression = factory
           .querySubtree(InstanceIdentifier.create(Root.class))
           .extractChild(CaseBContainer.class)
           .matching()
           .leaf(CaseBContainer::getKeyCb)
           .matchesPattern(Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$",
                                           Pattern.CASE_INSENSITIVE))
           .build();

       QueryExecutor executor = createExecutor();
       QueryResult<CaseBContainer> result = executor.executeQuery(queryExpression);

       assertEquals(1, result.getItems().size());

Binding Query can be used to filter important data, as is shown in previous examples. With Binding Query, it is possible to filter data with various options and get all the required information. Binding Query can also support matching string by patterns and simple filter operations with numbers.


by Peter Šuňa | Leave us your feedback on this post!

You can contact us at https://pantheon.tech/

Explore our Pantheon GitHub.

Watch our YouTube Channel.

© 2023 PANTHEON.tech s.r.o