Developer Guide

Contents

Json Schema Types

All components which contribute to the String output of the the Schema are ‘members’. They are nested under a schema, and during generation are recursively processed (top down), appending each members result to a json writer creating the final json output String.

Top level types UML

The different types of json schema member which may be nested under a schema. These are logically grouped by common behavior.

Member type structure

When developing against the toolkit, most of these classes are open to extension.

Example 1

A requirement arose for a new key/value pair to represent a devices startup time as a string value. You might simply extend BJsonSchemaProperty<T> as type <String> using your own date format, or type <BAbsTime> allowing the schema to render the date automatically using the schema date config. Now you just need to implement implement getJsonValue() to return the appropriate value.

@NiagaraType
public class BDeviceTimeProperty extends BJsonSchemaProperty<BAbsTime>
{
/*+ ------------ BEGIN BAJA AUTO GENERATED CODE ------------ +*/
.....
/*+ ------------ END BAJA AUTO GENERATED CODE -------------- +*/

  @Override
  public BAbsTime getJsonValue()
  {
    return (BAbsTime) ..... // this will use the schemas date format config
  }
}

Example 2

The requirement is for an object that contains a key/value pair for each slot on the target component, but only those with a user defined 1 flag. You might extend BJsonSchemaBoundObject, hide the slotsToInclude slot, and override the method getPropertiesToIncludeInJson() to only return properties with the user defined flag.

@NiagaraType
@NiagaraProperty(name = "slotsToInclude", type = "jsonToolkit:SlotSelectionType", defaultValue = "BSlotSelectionType.allVisibleSlots", flags = Flags.HIDDEN, override = true)
public class BUserDefinedFlags extends BJsonSchemaBoundObject
{
/*+ ------------ BEGIN BAJA AUTO GENERATED CODE ------------ +*/
.....
/*+ ------------ END BAJA AUTO GENERATED CODE -------------- +*/

  @Override
  public List<String> getPropertiesToIncludeInJson(BComplex resolvedTarget)
  {
    if (resolvedTarget == null)
    {
      return Collections.emptyList(); // or try to resolve it!
    }
    return Arrays.stream(resolvedTarget.getPropertiesArray())
      .filter(prop -> (resolvedTarget.getFlags(prop) & Flags.USER_DEFINED_1) != 0)
      .map(prop -> prop.getName())
      .collect(Collectors.toList());
  }
}

Relative Topic Builder

If the recipient requires a different topic or URL per point or device, the Relative Topic Builder is an example of building a topic (for mqtt) or path (for HTTP url) as the current base item output from a Relative Schema changes. The program object* can be found in the Program folder of the jsonToolkit palette.

As an example: link from the RelativeJsonSchema’s Current Base Output topic into the Base Item Changed property, and then from the output slot to the publish points proxyExt. Now the topic is updated for each item returned by the base query.

Other properties of the base could be inserted to the topic as desired (not just the name).

The shipped example illustrates the %s variable substituted by this: "/dummy/mqtt/example/%s"

Type Override Example

At the core of the jsonToolkit is a method which maps baja object types to Json. This determines, for example, how any BControlPoint, Facets, BAbsTime etc encountered should be encoded in the output.

Of course there will be many variations in payload for the supported Niagara types. Our approach to accommodating this is to allow a Developer, or power user, the ability to override specific types as they are converted to JSON.

For a small JsonSchema deployment this can be handled using the example shipped in the jsonToolkit palette which demonstrates how a program object[^1] can be used to replace Units:

 /**
   * Allows Json types to to be overridden when placed under JsonSchema/config/overrides/
   */
  public BValue onOverride(final BValue input)
  {
    if (input instanceof BUnit)
    {
      javax.baja.units.UnitDatabase unitDB = javax.baja.units.UnitDatabase.getDefault();
      javax.baja.units.UnitDatabase.Quantity quantity = unitDB.getQuantity(input.as(BUnit.class));
      if (quantity != null)
      {
        return BString.make(input.as(BUnit.class).getSymbol() + ":" + quantity.getName());
      }
    }

    // If we can't override the value then just return it as we found it
    return input;
  }

[^1]: The ProgramBuilder should be used in any event that a program object is duplicated repeatedly to improve maintainability and station loading time.

To use simply drag this into the Overrides config folder of the schema.

Override

Developers could also override the doOverride(BValue value) method in their own BTypeOverride variant.

InlineJsonWriter

Allows the schema to defer control to their own code in the tree of Schema members, meaning dynamic content of any form desired can be added into the Schema output.

This can be achieved using a program object* (* see note above), as per the example in the Program folder of the jsonToolkit palette, or developers can extend BAbstractInlineJsonWriter. Extending the abstract class would be preferred where the program object may be widely distributed, as code contained in a module is easier to maintain.

The example in the palette implements a method public BValue onOverride(final BInlineJsonWriter input) which should be customized to meet the projects needs. The InlineJsonWriter has two important methods:

Demonstrated below:

/**
 * The override method allows control of the writer and current base to be passed to the code below
 * allowing JSON to be dynamically constructed within a schema.
 *
 * @param BInlineJsonWriter wraps two things:
 *           JSONWriter jsonWriter = in.getJsonWriter();
 *           BComplex base = in.getCurrentBase();
 *            
 * @return BValue allows logging of the "result" when fine logging is enabled
 * (this does not need to match what happened to the JSON...)
 */
public BValue onOverride(final BInlineJsonWriter in)
{
    // current base is set by the parent schema as each point is submitted for publishing
    BComplex base = in.getCurrentBase();
    
    //if (base instanceof BComponent)    
    
    JSONWriter jsonWriter = in.getJsonWriter();
    
    jsonWriter.key("highLimit");
    jsonWriter.value("1024");
    
    // do not close writer
      
    return null;
}

Custom Query Style

It is likely that 3rd party systems may require query results to be formatted in a manner other than the options shipped with the jsonToolkit.

It is possible to render query data differently by extending BQueryResultWriter and registering the class as an agent on jsonToolkit:JsonSchemaBoundQueryResult:

Below is a moovelous example showing how the contents of the QueryResultHolder can be formatted as required by the external system:

package com.tridiumx.jsonToolkit.outbound.schema.query;

import static com.tridiumx.jsonToolkit.outbound.schema.support.JsonSchemaUtil.toJsonType;

import java.util.concurrent.atomic.AtomicInteger;
import javax.baja.nre.annotations.AgentOn;
import javax.baja.nre.annotations.NiagaraType;
import javax.baja.sys.BString;
import javax.baja.sys.Sys;
import javax.baja.sys.Type;

import com.tridiumx.jsonToolkit.outbound.schema.query.style.BQueryResultWriter;
import com.tridium.json.JSONWriter;

/**
 * An example custom query result writer.
 *
 * @author Nick Dodd
 */
@NiagaraType(agent = @AgentOn(types = "jsonToolkit:JsonSchemaBoundQueryResult"))
public class BCowSayJson extends BQueryResultWriter
{
/*+ ------------ BEGIN BAJA AUTO GENERATED CODE ------------ +*/
/*@ $com.tridiumx.jsonToolkit.outbound.schema.query.style.BObjectsArray(4046064316)1.0$ @*/
/* Generated Thu Dec 13 11:24:58 GMT 2018 by Slot-o-Matic (c) Tridium, Inc. 2012 */

////////////////////////////////////////////////////////////////
// Type
////////////////////////////////////////////////////////////////
  
  @Override
  public Type getType() { return TYPE; }
  public static final Type TYPE = Sys.loadType(BCowSayJson.class);

/*+ ------------ END BAJA AUTO GENERATED CODE -------------- +*/

  @Override
  public BString previewText()
  {
    return BString.make("A demonstration result writer");
  }

  @Override
  public void appendJson(JSONWriter jsonWriter, QueryResultHolder result)
  {
    jsonWriter.object();

    try
    {
      jsonWriter.key("mooo01").value("____________________________");
      headerCsv(jsonWriter, result);
      dataCsv(jsonWriter, result);
      jsonWriter.key("mooo02").value("----------------------------");
      jsonWriter.key("mooo03").value("    \\   ^__^               ");
      jsonWriter.key("mooo04").value("     \\  (oo)\\_______      ");
      jsonWriter.key("mooo05").value("         (__)\\       )\\/\\");
      jsonWriter.key("mooo06").value("              ||----w |     ");
      jsonWriter.key("mooo07").value("              ||     ||     ");
    }
    finally
    {
      jsonWriter.endObject();
    }
  }

  private void headerCsv(JSONWriter jsonWriter, QueryResultHolder result)
  {
    jsonWriter.key("columns").value(String.join(",", result.getColumnNames()));
  }

  private void dataCsv(JSONWriter jsonWriter, QueryResultHolder result)
  {
    AtomicInteger rowCount = new AtomicInteger();

    result.getResultList().forEach( map -> {

      jsonWriter.key("data" + rowCount.incrementAndGet());
      jsonWriter.array();
      try
      {
        map.values()
          .forEach(value -> jsonWriter.value(toJsonType(value, getSchema().getConfig())));
      }
      finally
      {
        jsonWriter.endArray();
      }
    });

    processChildJsonMembers(jsonWriter, false);  // append any nested members content to the json
  }
}

Builder Class / API

To support the programmatic creation of json schemas by developers, the JsonSchemaBuilder class provides suitable methods. For example:

  BJsonSchema schema =
        new JsonSchemaBuilder()
          .withUpdateStrategy(BJsonSchemaUpdateStrategy.onDemandOnly)
          .withQuery("Bacnet Query", "station:|slot:/Drivers/BacnetNetwork|bql:select out.value AS 'v', status from control:ControlPoint")
          .withRootObject()
          .withFixedNumericProperty("Version", BDouble.make(1.23))
          .withFixedObject("Config")
          .stepDown()
            .withBoundProperty("BacnetAddress", BOrd.make(String.format("station:|slot:/Drivers/BacnetNetwork/%s/address", deviceName)))
            .withBoundObject("DeviceSettings", BOrd.make(String.format("station:|slot:/Drivers/BacnetNetwork/%s/config/deviceObject", deviceName)))
          .stepUp()
          .withBoundQueryResult("Data", "Bacnet Query", BObjectsArray.TYPE.getTypeSpec())
      .build();

Would result in a schema with output like:

    {
      "Version":1.23,
      "Config":{
        "BacnetAddress":"192.168.1.24",
        "DeviceSettings":{
          "pollFrequency":"Normal",
          "status":"{ok}",
          "faultCause":"",
          "objectId":"device:100171",
          …….
    }
    },
    "Data":[
       {
         "v":0.45,
         "status":"{down,stale}"
       },
       ……*
       ]
    }

^ Note example trimmed for demonstration purposes

Useful methods

These are some methods a developer might regularly use when creating custom content.

Class Method Usage
BIJsonSchemaMember getJsonName() Override to return a different key. This will skip the schemas config settings for name case/space handling
BIJsonSchemaMember process(JSONWriter json, boolean jsonKeysValid) Override to append bespoke content to the current json stream via json.key() / json.value() etc. The boolean parameter indicates if keys are currently valid syntax (e.g not inside an array)
BJsonSchemaMember onSchemaEvent(BSchemaEvent event) Override to react to events such as the base item changing or subscription disabled
BJsonSchemaMember getSchema() Use to quickly get a reference to the parent schema
JsonSchemaNameUtil writeKey(BIJsonSchemaMember member, JSONWriter jsonWriter, String name) Write a json key with the schemas current case/space handling rules applied
JsonSchemaUtil toJsonType(Object value, BJsonSchemaConfigFolder config) Converts any java value to a native json type (String/Number/Boolean) with some default handling of some baja types, and filters out sensitive types
JsonSchemaUtil toBValue(Object value) Converts core java type values (Numerics/Strings/Booleans) to BValue equivalents. Returns a copy if the parameter is already a BValue.
BJsonSchemaBoundMember getOrdTarget() / getTarget() Get a live resolved reference to the ord bindings target
BJsonSchemaBoundMember handleSubscriptionEvent(Subscription subscription, BComponentEvent event) Override the schemas default behaviour for handling a subscription event from a binding target. The schemas default behaviour is to unsubscribe/ignore/request schema generation depending on the content
BJsonSchemaBoundSlotsContainer getPropertiesToIncludeInJson(target) Override to return a different set of slot values for the resolved target
BIPostProcessor postProcess(BJsonSchema schema, Exception exception) Implement to perform any lifecycle/cleanup/reporting task after a schema has completed output generation (or failed in which case the exception is non null)
JsonKeyExtractUtil lookup*() Various methods for extracting values from incoming json payloads
BJsonInbound routeValue(BString message, Context cx) Implement to handle an incoming json payload, throw RoutingFailedException if unable to process the message
BJsonSetPointHandler lookupTarget(BString msg, String id) Override to locate a control point by another means than the handle ord, e.g by slot path or name

How Schema Generation Works

There are 2 actions which cause the json schema to regenerate it’s output. This charts the flow through the schema logic.

Top level types UML

Binding ords are resolved against the current base item of the schema. This is the Station, unless using a relative json schema, in which case the current result of the base query is used. Currently base query are resolved against the Station.

Relative Schemas

External access to Schema output

A URL like the following also allows access to the Schema output via the JsonExporter:

http://127.0.0.1/ord/station:%7Cslot:/JsonSchema%7Cview:jsonToolkit:JsonExporter

This could allow access to an external application consuming data from Niagara.

Working with Apache Velocity

Apache Velocity can be used to expose the output of a Json Schema via the Jetty Web Server in Niagara 4. This may be beneficial for applications expecting to consume data provided by the Niagara Station, for example a Visualization or machine learning library.

Note: Given json’s origin as a data exchange format for the web, there are many libraries expecting to receive input in this format.

Take for example the Google Chart library. The example below is copied from the project website.

You can see the var data is populated with json data. Replacing this hard coded data with the output from a suitable configured JSON Schema in your station will allow a chart to be drawn from Niagara Station data.

<html>
  <head>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    <script type="text/javascript">
      google.charts.load('current', {'packages':['corechart']});
      google.charts.setOnLoadCallback(drawChart);
      function drawChart() {
        var data = google.visualization.arrayToDataTable([
          ['Year', 'Sales', 'Expenses'],
          ['2004',  1000,      400],
          ['2005',  1170,      460],
          ['2006',  660,       1120],
          ['2007',  1030,      540]
        ]);
        var options = {
          title: 'Company Performance',
          curveType: 'function',
          legend: { position: 'bottom' }
        };

        var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));

        chart.draw(data, options);
      }
    </script>
  </head>
  <body>
    <div id="curve_chart" style="width: 900px; height: 500px"></div>
  </body>
</html>

Steps to build a chart

    var data = google.visualization.arrayToDataTable([
        $schema.output
    ]);

So, what did we achieve? The template HTML file has a variable, which when accessed via the stations velocity servlet will be replaced with the output from our schema.

If you add a “WebBrowser” from the Workbench palette onto a Px Page, then set the ord property to http:\\127.0.0.1\velocity\chart you should see a chart when you view that page in a web browser. If not, you can use the developer tools to view the source code and ensure that the $schema.output variable is being replaced with the output of your schema.

Subscribing to schema output with bajascript

Whilst Velocity is a very convenient means to inject data into a html document, one of many benefits of using bajascript in your application is support for subscription - this lets the graphic update as data changes.

The example below uses the Chart.js library to demonstrate.

Of course the schema output could also be built in bajascript by executing queries or directly subscribing to the components required, but the jsonSchema may reduce some of the work needed in JavaScript, allowing subscription only to the output slot which can fetch the required data from the station.

Example html file for showing Chart.js

<!DOCTYPE html>
<!-- @noSnoop -->
<html>
<head>
  <title>HTML Page</title>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>

  <script type='text/javascript' src='/requirejs/config.js'></script>
  <script type='text/javascript' src='/module/js/com/tridium/js/ext/require/require.min.js?'></script>

  <!-- note the special syntax for downloading JS file from the 'bajascript' folder you add in your station -->
  <script type='text/javascript' src='/ord/file:%5Ebajascript/basic.js%7Cview:web:FileDownloadView'></script>

</head>
<body>

  <canvas class="my-4 w-100" id="myChart" width="800" height="450"></canvas>

</body>
</html>

Example basic.js file to fetch chart data

The ‘data’ array in the below payload uses bound properties, a Single Column query would allow historical data to be used instead from a bql query on the history db.

// Subscribe to a Ramp. When it changes, print out the results.
require(['baja!'], function (baja) {
    "use strict";

    // A Subscriber is used to listen to Component events in Niagara.
    var sub = new baja.Subscriber();

    var update = function () {

      // Graphs
      var ctx = $('#myChart');

      var newJson = JSON.parse(this.getOutput());

      var myChart = new Chart(ctx, newJson)
    };

    // Attach this function to listen for changed events.
    sub.attach('changed', update);

    // Resolve the ORD to the Ramp and update the text.
    baja.Ord.make('station:|slot:/ChartsJS/JsonSchema').get({ok: update, subscriber: sub});
    });

Example Schema Output for Chart

{
  "type": "line",
  "data": {
    "labels": [
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday"
    ],
    "datasets": [
      {
        "data": [
          202,
          240,
          202,
          3,
          150
        ],
        "backgroundColor": "transparent",
        "borderColor": "#007bff",
        "borderWidth": 3,
        "lineTension": 0
      },
      {
        "data": [
          3,
          202,
          150,
          202,
          240
        ],
        "backgroundColor": "transparent",
        "borderColor": "#ff0033",
        "borderWidth": 3,
        "lineTension": 0
      }
    ]
  },
  "options": {
    "scales": {
      "yAxes": [
        {
          "ticks": {
            "beginAtZero": false
          }
        }
      ]
    },
    "legend": {
      "display": false
    },
    "title": {
      "display": true,
      "text": "Philips Hue Light Demo"
    },
    "tooltips": {
      "intersect": true,
      "mode": "index"
    },
    "hover": {
      "intersect": true,
      "mode": "nearest"
    }
  }
}

Inbound Components

To create a new inbound types simply extend one of the 3 main types: BJsonRouter, BJsonSelector or BJsonHandler and implement routeValue(BString message, Context cx) throws RoutingFailedException

Throw a new RoutingFailedException at any stage to report an error and update the lastResult slot.

When extending any of the BJsonInbound types you may specify which property will trigger an automatic re-routing of the last input with Property[] getRerunTriggers().

The helper interface JsonKeyExtractUtil contains several methods for extracting values from a JSON payload.