Qualifiers and Property Bags in Detail

We have seen property bags and qualifiers in action in various examples. Property bags are to CaseMaster what JSON is to Javascript.

In this chapter we will look a property bags and qualifiers (a special type of property bag) in more detail.

The Anatomy of a Property Bag

The simplest property bag is the empty property bag and is created as follows:

    set( 'emptyPB', <> )

A property consists of zero of more entries where an entry may have a name. The following shows a property bag with 3 entries of which 2 are named and one is not.

    set(
        'PB',
        <
            entry1: 'Hello gr'
            entry2: 8
            'world'
        >
    )

The 3 entries all exist at 'level 1'. If an entry has a name, the name must be unique at its level. Take the following example:

Note: the function pb.count() returns the number of entries at level 1 of a property bag.

    set(
        'PB',
        <
            entry1: 'Hello gr'
            entry2: 8
            'world'
        >
    )

    response.write( pb.count( [PB] ) )              
    // Writes 3

    response.write( '<br>' )

    set(
        'PB',
        <
            entry1: 'Hello gr'
            entry1: 8
            'world'
        >
    )

    response.write( pb.count( [PB] ) )              
    // Writes 2

A property bag entry can be another property bag and thus create another level:

    set(
        'PB',
        <
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
        >
    )

    response.write( pb.count( [PB] ) )
    // Writes 2

Using Expressions in Property Bags

Each of the entries in the previous example is a literal value (such as 12, 'Joe Smith' or the date #12 June 1992#). Literals are the simplest of expressions. Entries can also be more complex expressions:

    set(
        'PB',
        <
            example: today()
        >
    )

    response.write( [PB] )
    // Writes date of today

Note the behaviour when you refer to a property bag as a string (as we do in the response.write() in the above example). CaseMaster will simply return the string value of the first entry.

$-Expressions

We have already seen $-expressions in action for the dynamic value and default value of an attribute (see here).

Expressions in a property bag are resolved when the property bag is first used. For performance, expressions are cached when they are first evaluated and the cached value is used moving forward.

Using a $-expression instructs CaseMaster to not cache the value but re-evaluate the expression on every use. This is demonstrated in the following example:

    set(
        'PB',
        <
            example: random()
        >
    )

    response.write( [PB] )
    response.write( '<br>' )
    response.write( [PB] )
    response.write( '<br>' )

    set(
        'PB',
        <
            example: $random()
        >
    )

    response.write( [PB] )
    response.write( '<br>' )
    response.write( [PB] )
    response.write( '<br>' )

    // Possible output:
    // 0.248785048373409
    // 0.248785048373409  (the same, without the $)
    // 0.700962220179365
    // 0.0291897929409471 (different because of the $)

Getting Stuff from Property Bags

The function pb.get() is used to retrieve entries from a property bag. The function takes two parameters: a property bag and a path.

    set(
        'PB',
        <
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
        >
    )

    response.write( pb.get( [PB], '1/id' ) )            // 1
    response.write( '<br>' )
    response.write( pb.get( [PB], '2/id' ) )            // 2
    response.write( '<br>' )
    response.write( pb.get( [PB], '-1/name' ) )         // 3
    response.write( '<br>' )
    response.write( pb.get( [PB], '-2/dob' ) )          // 4
    response.write( '<br>' )
    response.write( pb.get( [PB], '1' ) )               // 5
    // Output
    // 12
    // 18
    // Mary Jane
    // 1972-03-05
    // 12
  1. Get the id from the first entry
  2. Get the id of the second entry
  3. Get the name of the last entry
  4. Get the dob of the sceond last entry
  5. Get entry 1 (which is a property bag and is treated as a string and thus return the first entry being 12)

Putting Stuff in Property Bags

The opposite of pb.get() is, obviously, pb.set().

The following results in the same property bag as in the previous example:

    set(
        'joe',
        <
            id: 12
            name: 'Joe Smith'
            dob: #5 mar 1972#
        >
    )

    set(
        'mary',
        <
            id: 18
            name: 'Mary Jane'
            dob: #12 June 1992#
        >
    )

    set( 'PB', < > )

    pb.set( [PB], '1', [joe] )
    pb.set( [PB], '2', [mary] )

The pb.set() function takes 3 or 4 parameters but the version with 3 parameters is most commonly used.

Parameter Use
Property bag Handle to a property bag
Path Valid path
Value Value to set entry to

The following example shows how to set values at sub-levels:

    set(
        'joe',
        <
            id: 12
            name: 'Joe Smith'
            dob: #5 mar 1972#
        >
    )

    set(
        'mary',
        <
            id: 18
            name: 'Mary Jane'
            dob: #12 June 1992#
        >
    )

    set( 'PB', < > )

    pb.set( [PB], '1', [joe] )
    pb.set( [PB], '2', [mary] )

    pb.set( [PB], '2/name', 'Joe Bloggs' )
    pb.set( [PB], '1/dob', #5 dec 2000# )

    response.write( pb.get( [PB], '1/id' ) )
    response.write( '<br>' )
    response.write( pb.get( [PB], '2/id' ) )
    response.write( '<br>' )
    response.write( pb.get( [PB], '-1/name' ) )
    response.write( '<br>' )
    response.write( pb.get( [PB], '-2/dob' ) )
    response.write( '<br>' )
    response.write( pb.get( [PB], '1' ) )
    // Output
    // 12
    // 18
    // Joe Bloggs
    // 2000-12-05
    // 12

Property Bags as Arrays and Matrices

Property bags can be used as arrays. When you add entries to the same level without a name, you effectively create an array.

    set(
        'joe',
        <
            id: 12
            name: 'Joe Smith'
            dob: #5 mar 1972#
        >
    )

    set(
        'mary',
        <
            id: 18
            name: 'Mary Jane'
            dob: #12 June 1992#
        >
    )

    set( 'PB', < > )

    pb.set( [PB], null(), [joe] )
    pb.set( [PB], null(), [mary] )

    response.write( pb.count( [PB] ) )
    response.write( '<br>' )
    // Output
    // 2    

Needless to say that you can create matrices by nesting property bag arrays inside property bags.

Iterator.ofPB()

The iterator.ofPB() function enables yo you iterate over the entries in a property bag.

    set(
        'PB',
        <
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
        >
    )

    iterate iterator.ofPB( [PB], 'item' )

        iterate iterator.ofPB( [item], 'entry' )
            response.write( [entry] )
            response.write( '<br>' )
        end-iterate

    end-iterate

    // Output
    // 12
    // Joe Smith
    // 1972-03-05
    // 18
    // Mary Jane
    // 1992-06-12

You can actually access the key of each entry if you make sure you have a handle to the iterator:

    set(
        'PB',
        <
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
        >
    )

    iterate iterator.ofPB( [PB], 'item' )

        set( 'iterator', iterator.ofPB( [item], 'entry' ) )
        iterate [iterator]
            response.write( concat( iterator.key( [iterator] ), ' = ', [entry] ) )
            response.write( '<br>' )
        end-iterate

    end-iterate

    // Output
    // 12
    // Joe Smith
    // 1972-03-05
    // 18
    // Mary Jane
    // 1992-06-12

Property Bag and JSON

The function json.pb2json() can be used to turn a property bag into JSON.

    set(
        'PB',
        <
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
        >
    )

    response.write( page.htmlEncode( json.pb2json( [PB] ) ) )
    // Output:
    // {
    //     "9c6b7036-ca72-4fcd-a79c-fffb5f68a25c": { 
    //         "id": 12, 
    //         "name": "Joe Smith", 
    //         "dob": "1972/03/05" 
    //     }, 
    //     "ea7cce41-cedd-4c2a-86e2-2104d5f80f3a": { 
    //         "id": 18, 
    //         "name": "Mary Jane", 
    //         "dob": "1992/06/12" 
    //     } 
    // }

Note the GUID's that CaseMaster had to add to ensure the JSON is valid.

The solution is to use <@json/array> and make sure the array is not the outer level of the property bag:

    set(
        'PB',
        <
            array: <@json/array
                <
                    id: 12
                    name: 'Joe Smith'
                    dob: #5 mar 1972#
                >
                <
                    id: 18
                    name: 'Mary Jane'
                    dob: #12 June 1992#
                >
            >
        >
    )

    response.write( page.htmlEncode( json.pb2json( [PB] ) ) )
    // Output:
    // { 
    //     "array": [ 
    //         { "id": 12, "name": "Joe Smith", "dob": "1972/03/05" }, 
    //         { "id": 18, "name": "Mary Jane", "dob": "1992/06/12" } 
    //     ] 
    // }

You can also turn JSON into a property bag:

    set(
        'PB',
        <@json
            array: <@json/array
                <
                    id: 12
                    name: 'Joe Smith'
                    dob: #5 mar 1972#
                >
                <
                    id: 18
                    name: 'Mary Jane'
                    dob: #12 June 1992#
                >
            >
        >
    )

    response.write( page.htmlEncode( pb.dump( json.json2pb( json.pb2json( [PB] ) ) ) ) )
    // Output:
    // <@json 
    //     array: <@json/array 
    //         <@json id: 12, name: "Joe Smith", dob: "1972/03/05">, 
    //         <@json id: 18, name: "Mary Jane", dob: "1992/06/12">
    //     >
    // >

Advanced Property Bag Functions

Function Usage
pb.parse() Parse a string that represents a property bag and turn it into a property bag
pb.dump() Dump a property bag to a string
pb.first() Get the first entry of a property bag; optionally the first to match a lambda expression
pb.last() Get the last entry of a property bag; optional lambda expression
pb.getRandom() Get a random entry of a property bag; optional lambda expression
pb.filter() Create a new property bag filtered by a lambda expression
pb.sort() Create a new property bag ordered by a lambda expression

The following example shows pb.filter() in action:

    set(
        'PB',
        <
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
        >
    )

    response.write( pb.filter( [PB], lambda.create( function ( a ) return match( pb.get( [a], 'name' ), 'Mary' )  end-function ) ) )
    // Output:
    // 18

The following example shows pb.sort() in action:

    set(
        'PB',
        <
            <
                id: 18
                name: 'Mary Jane'
                dob: #12 June 1992#
            >
            <
                id: 7
                name: 'John Smith'
                dob: #21 May 1987#
            >
            <
                id: 12
                name: 'Joe Smith'
                dob: #5 mar 1972#
            >
        >
    )

    response.write( 
        page.htmlEncode( 
            pb.dump( 
                pb.sort( 
                    [PB], 
                    lambda.create( 
                        function ( a, b ) 
                            return sub( 
                                pb.get( [a], 'id' ), 
                                pb.get( [b], 'id' ) 
                            )  
                        end-function 
                    ) 
                ) 
            ) 
        ) 
    )
    // Output:
    // <<id: 7, name: "John Smith", dob: #1987-05-21#>, <id: 12, name: "Joe Smith", dob: #1972-03-05#>, <id: 18, name: "Mary Jane", dob: #1992-06-12#>>

Qualifiers

We have already seen qualifiers in action. Take for example a look at the following attribute definition from a BO descriptor:

    email: <@bo/attribute/email
        label: <@mltext 'Email'>
        column: 'email'
    >

The @-sign indicates that we are dealing with a qualifier. A qualifier is a special type of property bag or property bag entry.

In the above example the type of the qualifier is @bo/attribute/email, this means that the system has access to a file bo/attribute/email.cms in the qualifier folder. This file is the implementation of the qualifier.

Qualifiers are very powerful yet rather abstract and thus difficult to explain.

One obvious, and easily explained, benefit of a qualifier is that the VSCode Casemaster® extension can provide code suggestions.

qualifier-intellisense

It can do so because a qualifier .cms may contain a schema resource describing the tags this qualifier property bag expects.

The following is an example of a schema resource:

    resource schema
        <@qualifier/schema
            summary: 'Defines a multi language text value',
            items: <
                text: <@qualifier/schema/item
                    name: 'text',
                    summary: 'Default language text value',
                >,
                key: <@qualifier/schema/item
                    name: 'key',
                    summary: 'Key for translations file',
                >,
                parms: <@qualifier/schema/item
                    name: 'parms',
                    summary: 'Parameters for formatting string'
                >
                language: <@qualifier/schema/item
                    name: 'language',
                    summary: 'Force language; otherwise use user language'
                >
                layer: <@qualifier/schema/item
                    name: 'layer'
                    summary: 'Specify a specific layer if not the main system'
                >
                strict: <@qualifier/schema/item
                    name: 'strict',
                    summary: 'Save text strictly as provided, case sensitive and otehr characters'
                >
            >
        >
    end-resource

A schema has 4 optional sections:

Section Use
summary A brief summary of the purpose of the qualifier, shown by VSCode as hint
documentation A more verbose explanation of the qualifier, shown in online help
insertText Text that is inserted in a document when selecting code completion in VSCode
items The accepted tags in the qualifier

The most important section is the items section which is (as seen in the previous example) a collection of @qualifier/schema/item qualifier tag.

Tag Use
name The name of the tag (mandatory)
summary Summary, used by VSCode to show as hint
documentation More verbose documentation, used by online help
defaultValue Default value when the tag is omitted
qualifier Name of qualifier if the tag itself is a qualifier (defaults to no qualifier)
mandatory Indicates whether this tag is mandatory or optional (defaults to optional)

The following is an example:

    <@qualifier/schema/item
        name: 'label'
        summary: 'Defines the attributes label'
        documentation: 'The label of an attribute either as a simple string, @text or @mlText'
        defaultValue: qualifier.call('./defaultValue', 'label')
        qualifier: '@text'
        mandatory: true()
    >

Qualifier inheritance

A qualifier can inherit from another qualifier. A common use of this feature is to override / set certain default values. Take for example the qualifier bo/attribute/boolean which inherits from bo/attribute.

inherits '@bo/attribute'

resource schema
    <@qualifier/schema
        summary: 'Defines a business object boolean attribute',
        items: qualifier.get('../schema', 'items')
    >
end-resource

resource defaultValues
    <
        dataType: dataType.Boolean,
        defaultValue: false(),
        length: 1,
        optional: false(),
        outputMask: 'Yes/No',
        displaySize: <@bo/attribute/property/displaySize/md>
    >
end-resource

The scheme simply gets the items from the schema from its parent (using the ../ syntax) and provides a number of meaningful default values for certain tags.

Qualifier Default Values

The defaultValues trick as shown in the bo/attribute/boolean example is just making clever use of CaseMaster® 2.0 features but proofs to be a very useful trick in qualifiers especially where you expect sub-classes.

The following entry in the schema/items section of bo/attribute shows it in action:

    <@qualifier/schema/item
        name: 'multiline',
        summary: 'Defines if the attribute is multiline',
        defaultValue: qualifier.call('./defaultValue', 'multiline')
    >

This trick relies on the following function and resource to be present in the qualifier:

function defaultValue(name)

    if qualifier.tryGet('./defaultValues', [name], 'value')
        return [value]
    else-if qualifier.tryGet('@bo/attribute:defaultValues', [name], 'value')
        return [value]
    end-if

    return null()

end-function

resource defaultValues
    <
        label: null()
        column: null()
        virtualColumn: false()
        dataType: null()
        foreignKey: null()
        foreignKeyLabelGroup: 'label'
        defaultValue: null()
    >
end-resource

Review the bo/attribute/boolean example again:

  • bo/attribute/boolean inherits from bo/attribute and inherits the schema items
  • bo/attribute/boolean has its own defaultValues resource
  • The function defaultValue first checks the local defaultValues resource and, when nothing is found, gets the default value from the parent

\