Terracotta 10.11 | TCStore API Developer Guide | Reference | Concepts | Working with Complex Data Types
 
Working with Complex Data Types
Two of the TCStore types are Complex Data Types (CDTs): LIST and MAP. These are represented programmatically as instances of StoreList and StoreMap. CDTs share some common attributes:
*the values in CDTs are comprised of TypedValue instances, consisting of TCStore Type and a value of the corresponding type.
*CDTs can contain other CDT values, as well as the more typical scalar types.
*they can be immutable or mutable, and a new deep-copy mutable instance can be created from any instance via the mutableCopy() method.
*all support find(spec) Selection querying for efficient selecting of arbitrary data inside CDTs by type, position, and key, in various combinations.
*they can be exported to a text representation via the toString(boolean skipWhitespace) method
*the text representation can be parsed into complex data objects via the ComplexDataParser class, as well as various helper methods on StoreList, StoreMap, and ComplexRecord.
The StoreMap/StoreList hierarchy can be thought of as a JSON-like data structure with more specific typing (JSON Number becomes Integer, Long, Double,) and extra types (Character, byte[]), and no null values.
Important Classes
TypedValue
As stated, CDTs are comprised of TypedValue instances. Cell is a subclass of TypedValue as well, and bare TypedValue instances can be constructed by various helper methods on TypedValue.
StoreMap
The StoreMap interface defines a Java Map with String keys and TypedValue values. StoreMap objects come in two flavors, immutable and mutable. Typically, maps retrieved from map cells in the store will be immutable.
StoreList
The StoreList interface defines a Java List with TypedValue values. StoreList objects come in two flavors, immutable and mutable. Typically, lists retrieved from list cells in the store will be immutable.
ComplexRecord
ComplexRecord instances are synthetic representations of Record instances. Logically, any Record can be thought of as a ComplexMap plus the key for the Record (in fact, ComplexRecord implements ComplexMap just like StoreMap does). Any Record can be accessed as a ComplexRecord via the Record.asComplex() method. ComplexRecord objects retain the key typing of their source Record.
Finally, any ComplexRecord can be turned into a CellSet, suitable for adding to a Dataset (via the same method as StoreMap uses), or can be turned back into an immutable Record via the asRecord() helper method.
Construction and Manipulation of CDTs
CDTs fetched from a Dataset will be immutable. There are a variety of ways to make mutable copies or to create your own CDTs. Once a StoreList or StoreMap is mutable, it can be manipulated as if was an everyday Java List or Map, respectively.
StoreMap
To create a mutable StoreMap, use the StoreMap.newMap() helper method. Additionally, StoreMap instances can be constructed by parsing the text representation using the mapOf() helper methods. A StoreMap can also be constructed from a CellSet (remember, Record instances are CellSets) using a mapOf() helper.
Mutable StoreMap instances have functional with(xxx) methods for fluent construction.
For example, this code:
StoreMap m = StoreMap.newMap()
.with("name", "john")
.with("initial", 'R')
.with("age", 10)
.with("alive", true)
.with("marker", 100101010L)
.with("temp", 98.6d)
.with("img", new byte[] { 1, 2, 3, 4 });
produces this structure:
{
"name" : string : "john",
"initial" : char : 'R',
"age" : int : 10,
"alive" : bool : true,
"marker" : long : 100101010,
"temp" : double : 98.6d,
"img" : bytes : AQIDBA==
}
StoreList
To create a mutable StoreList, use the StoreList.newMap() helper method. Additionally, StoreList instances can be constructed by parsing the text representation using the mapOf() helper methods.
Mutable StoreList instances have functional with(xxx) methods for fluent construction.
So this code:
​StoreList l = StoreList.newList()
​.with("john")
​.with('R')
​.with(10)
​.with(true)
​.with(100101010L)
​.with(98.6d)
​.with(new byte[] { 1, 2, 3, 4 })
produces this structure:
[
string : "john",
char : 'R',
int : 10,
bool : true,
long : 100101010,
double : 98.6d,
bytes : AQIDBA==
]
ComplexRecord
Immutable ComplexRecord objects can be made mutable, just like other CDTs, via a mutableCopy() method. Changes to such a mutable copy are not, of course, reflected in the original source record.
Furthermore, mutable ComplexRecord objects can be constructed via the ComplexRecord.mutableRecord(K key, StoreMap cells) method. In this case, further modification is available through the same with(xxx) methods as StoreMap provides.
Filtering and Querying CDTs
Since CDTs can be arbitrarily complex, a method is needed to concisely identify subsets of their values for further evaluation. The find() method, coupled with the Selection class for holding filtered views of a CDT, provide a powerful mechanism for evaluating the contents of CDTs.
Selection Objects, a Simple Example
To provide concise selection, CDTs support a text based selection methodology, coupled with programmatic refinement of the results.
For example, assume a map structure like this:
{
"name" : map : {
"first" : string : "chris",
"last" : string : "jones",
"age" : int : 20,
"degrees" : list : [ "bs", "ms", "phd" ]
}
}
and this code fragment:
StoreMap m;

Selection sel1 = m.find("name.first");

Selection sel2 = m.find("name.[]");

Selection sel3 = m.find("name.[]").ints();

Selection sel4 = m.find("name.[]").ints().get(0);

Selection sel5 = m.find("name.degrees.[]").strings().last();

Selection sel6 = m.find("nam.degrees.[]")
sel1 will contain a single TypedValue, a String value of "chris".
sel2 will contain all 4 TypedValues, in the name keyed map.
sel3 will contain a single TypedValue, an integer value of 20.
sel4 will contain a single TypedValue, an integer value of 20.
sel5 will contain a single TypedValue, a String value of "phd".
sel6 will be empty.
A Selection object can be:
*restricted by any of the defined types in TCStore,
*can be accessed randomly by an integer index to return an Optional
*can be accessed randomly by a negative integer to return a value offset from the last entry, where get(-1) accessed the last entry, get(-2) accesses the second from last, etc.
*first and last entries can be accessed directly.
Selection objects are instances of Java Collection<TypedValue>, so they can be iterated over, have size() and contains() methods, etc. Refining a Selection object by any of the Comparable types in TCStore (selection.ints(), selection.strings(), etc), will return a ComparableSelection object, which then adds things like min(), max(), greaterThan(), etc. Refining to a Number type via ints(), longs(), or doubles() will return a specifically typed Number selection, that is both Comparable and adds the numeric method sum().
Selection object manipulation for querying is supported by the filtering DSL, and it is anticipated that much of this will be made portable for server side execution in the future.
find() and the dot-specification
In the above example, the dot specification was used to select a subset of the CDT. This concise, textual specification allows for the selection of subsets of CDTs in simple, intuitive manner. Simply put, a dot specification consists of a sequence of key names or array slices, separated by a period. So,
foo.[10].baz
starts at the outermost map, selects the value that has the key 'foo', treats it as a list, then uses the 11th indexed element (list references begin with 0) from the list, then treats the list element as a map, and selects the value with the key 'baz'.
Note that the manner of referencing at each level denotes typing as well; if the value found at 'foo' is not a list, then the Selection returned will be empty. find() calls cannot fail, except for parsing errors, they can however return an empty Selection.
Dot notation is intended to be both simple and powerful.
*'.' by itself matches the current object
*'.foo' if the top-level object is a map, matches the value of the 'foo' key
*'.foo.bar' matches the top-level object as a map, then the value at key 'foo', then matches if that value is a map, then matches the value of its key 'bar'. etc
*keys for matching can also be expressed with more explicit syntax for handling special characters:
*'.[foo bar].[baz]' to allow for spaces
*'.["foo\nbar"].baz' to allow for special characters
*'.[1]' matches the list entry at index 1 (lists are zero based)
*'.[1:3]' matches the slice from 1 to 3 inclusive
*'.[-2]' selects the second to last entry
*finally, a stanza of '.[]' matches all entries at this level
find() and its DSL variants all return Selection objects, which themselves allow further refining of the search results.
An important consideration when working with Selection objects and the DSL is that once you get to a typed scalar value (e.g., ...​find("name").strings().first()) you can then use that value as you would any other scalar value, for comparisons and such.