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.
The different types of json schema member which may be nested under a schema. These are logically grouped by common behavior.
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());
}
}
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"
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.
Developers could also override the doOverride(BValue value)
method in their own BTypeOverride
variant.
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:
JSONWriter jsonWriter = in.getJsonWriter();
BComplex base = in.getCurrentBase();
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;
}
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
}
}
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
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 |
There are 2 actions which cause the json schema to regenerate it’s output. This charts the flow through the schema logic.
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.
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.
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>
Create a new file chart.vm
and paste in the code example of a sample chart from the json consuming charting library of your choice
Replace the JSON data with a velocity variable, for example $schema.output
var data = google.visualization.arrayToDataTable([
$schema.output
]);
After saving the file, open the axVelocity palette and add a VelocityServlet named “chart” to your station.
Add a VelocityDocument below the servlet and change the Template File property to point at the chart.vm
file created earlier
Now add a new ContextOrdElement named Schema
to the VelocityContext of your VelocityDocument
The Schema Ord element should be updated to point at a suitable JsonSchema added to your station. The schema could output live station data, or the result of a query or transform. Both would be suitable for charting libraries, although it may be necessary to modify the time and date format from the Schema default settings or to reduce the presented interval of data by using a SeriesTransform Rollup function.
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.
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.
<!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>
basic.js
file to fetch chart dataThe ‘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});
});
{
"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"
}
}
}
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.