Collections

Overview of Changes from Niagara AX

There are several inadequacies in the Baja Collections API - javax.baja.collection and javax.baja.sys.Cursor. The current API suffers from a number of problems that hinder performance and encourage inefficient implementations for cases where data sets are large. The API changes aim to help developers be more productive with the collection API, and to pave the way for better implementation when underlying data sets are large.

Impacts

Any module that makes use of the javax.baja.collection classes or javax.baja.sys.Cursor is impacted by these changes. Depending on what methods and classes of the API the code uses you may need to refactor your code. Any implementations of BICollection, BIList, and BITable will be impacted by these changes. In the unlikely event that you implemented the javax.baja.bql.BIRelational interface in your code, you will also be impacted.

Changes

Removed BICollection

One of the biggest issues with Niagara AX’s Collections API is the BICollection interface. BICollection requires every implementation to model itself as a collection, a list, and a table. This puts a heavy burden on developers implementing a collection, and in many cases it does not make sense to model a list as a table, and vice-versa. So this interface has been removed entirely.

Code Impacts

The interface only had methods for converting the underlying collection to a list or table. Every implementation of BICollection in the framework now implements BITable. If you were casting objects to BICollection you should be able to safely cast them to BITable now. Almost invariably this was due to ord resolution of a bql query:

Niagara AX

BICollection result = (BICollection)BOrd.make("bql:select displayName").get(base);
BITable table = result.toTable();

Niagara 4

BITable table = (BITable)BOrd.make("bql:select displayName").get(base);

Any public methods that took a BICollection will need to be refactored to expect a BITable.

Removed BIList

This change is probably the most significant in terms of fundamental philosophy change. As part of the design philosophy for collections in Niagara 4, we wanted to discourage random-access methods. In fact, they have essentially been removed from the collection API in favour of cursor-based access. Don’t worry, you can still work with a table in a random-access way (details below). The ‘BIList’ interface essentially required random-access support for every collection. Further, an analysis of the entire framework showed that there were zero concrete implementations of BIList/BICollection in the public API that did not also implement BITable. This indicates that the BITable API is more useful to the framework as a whole.

Code Impacts

Similar to BICollection above, you should be able to cast any reference to a BIList to a BITable now. If by chance you had a public method that expected a BList, you will need to refactor that API to take a BITable.

Refactored BITable

The BITable interface has been greatly simplified and all random-access methods have been removed.

You can iterate the rows in the table by obtaining a TableCursor. The TableCursor gives you access to the table that contains the row, the Row object itself (see below), and a convenience method to obtain a cell value for the current row.

Each row in a table is modelled as a Row object. The row object gives you direct access to the underlying BIObject backing the row, as well as column cell values, flags, and facets.

Code Impacts

The biggest impact will occur if your code was iterating a BITable using the random-access methods of the old API. You have a few options.

First, change your code to iterate the table using a cursor. This is the best option.

// Iterate a BITable using a TableCursor
//
BITable table = (BITable)bqlOrd.resolve().get();
Column[] columns = table.getColumns().list();
try(TableCursor<BIObject> cursor = table.cursor())
{
  // Just for printing purposes, not for random access.
  int row = 0;
  while (cursor.next())
  {
    System.out.print(row + ": ");
    for (Column col : columns)
    {
      System.out.print(cursor.cell(col) + ", ");
    }
    System.out.println();
    ++row;
  }
}

If you must access the table using random-access indexing, you can convert it to a BIRandomAccessTable using the javax.baja.collection.Tables utility class.

BITable table = (BITable)ordThatResolvesToTable.resolve().get();
BIRandomAccessTable rat = Tables.slurp(table);
 
System.out.println(String.format("This table has %d rows", rat.size()));
for (int i=0; i<rat.size(); ++i)
{
  Row row = rat.get(i);
  // Do something with each row...
}

Refactored Cursor Interface

There are a few major changes to the javax.baja.sys.Cursor interface.

  1. javax.baja.sys.Cursor now implements java.lang.AutoCloseable. This means you should be a good citizen of every cursor you work with. Failing to close a cursor may result in a resource leak and degraded system performance. The try-with-resources statement introduced in Java 7 can help manage opening and closing cursors.
  2. Cursor is now generic: public interface Cursor<E> extends AutoCloseable
  3. This means it can iterate over any type; not just Niagara types.
  4. Since it can iterate any type, we removed the nextComponent() method from the interface and moved it into SlotCursor. This seems to be its primary use case anyway.
  5. A new javax.baja.sys.IterableCursor interface has been added that extends Cursor and implements Java’s Iterable interface. This enables a Cursor to be used in a for each statement as well as accessing the Cursor as an Iterator, Spliterator or Stream.
  6. javax.baja.sys.SlotCursor also implements the Iterable interface so it can be used to iterate over a collection of Slots (not BValue).

If you need to implement your own Cursor, use the utility class javax.baja.collection.AbstractCursor, which stubs out all methods in the interface and handles close semantics for you. You only need to provide an implementation of advanceCursor() and doGet().

Here are some example of the new SlotCursor design that use Java 8’s Stream API…

// Remove all dynamic Properties from a point...
point.getProperties()
  .stream()
  .filter(Slot::isDynamic)
  .forEach(point::remove);
 
// Print out the path string of all folders under a point...
point.getProperties()
  .stream()
  .map(point::get)
  .filter(v -> v.getType().is(BFolder.TYPE))
  .forEach(v -> System.out.println(v.asComponent().toPathString()));

BIRelational Interface Breaking Changes

In the unlikely event that you implemented the javax.baja.bql.BIRelational interface in your code, you will need to add a Context argument to its single method. The updated interface class is shown below:

public interface BIRelational<T extends BIObject>
{
  /**
   * Get the relation with the specified identifier.
   *
   * @param id A string identifier for the relation.  The format
   *   of the string is implementation specific.
   *
   * @param cx The Context associated with this request.
   *           This parameter was added starting in Niagara 4.0.
   *
   * @return Returns the relation identified by id or null if the relation
   *   cannot be found.
   */
  BITable<T> getRelation(String id, Context cx);
   
  Type TYPE = Sys.loadType(BIRelational.class);
}