/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/*global niagara:false*/
/**
* Defines {@link baja.Ord}.
* @module baja/ord/Ord
*/
define([
"bajaPromises",
"bajaScript/nav",
"bajaScript/baja/obj/Simple",
"bajaScript/baja/ord/OrdTarget",
"bajaScript/baja/comm/Callback" ], function (
Promise,
baja,
Simple,
OrdTarget,
Callback) {
"use strict";
var subclass = baja.subclass,
callSuper = baja.callSuper,
objectify = baja.objectify,
strictArg = baja.strictArg,
bajaDef = baja.def;
var VARIABLE_REGEX = "\\$\\(([^)]*)\\)?";
/**
* NCCB-27229
*
* When resolving a relativized Ord the default base should be the current
* session. Most of the time this is localhost, but in Workbench it could be
* any session (e.g. platform:).
*
* getSessionOrd() is injected by BWebWidget.
*/
function getDefaultBaseOrd() {
var sessionOrd;
if (baja.isOffline() &&
typeof niagara !== 'undefined' &&
niagara.env &&
typeof niagara.env.getSessionOrd === 'function') {
sessionOrd = niagara.env.getSessionOrd();
}
if (!sessionOrd) {
sessionOrd = 'local:';
}
return baja.Ord.make(sessionOrd);
}
/**
* Object Resolution Descriptor.
*
* An ORD is how we can access Objects in the Server from BajaScript. It's
* similar to a URI but is much more powerful and extensible. For more
* information, please see the Niagara developer documentation on ORDs and how
* they're used.
*
* If more than one ORD needs to be resolved then use a {@link baja.BatchResolve}.
*
* This Constructor shouldn't be invoked directly. Please use the `make()`
* methods to create an instance of an ORD.
*
* @see baja.Ord.make
* @see baja.BatchResolve
*
* @class
* @alias baja.Ord
* @extends baja.Simple
*
* @example
* <caption>Resolve an ORD</caption>
* baja.Ord.make("station:|slot:/Folder/NumericWritable").get({ lease: true })
* .then(function (numericWritable) {
* baja.outln(numericWritable.getOutDisplay());
* });
*/
var Ord = function Ord(ord) {
callSuper(Ord, this, arguments);
this.$ord = strictArg(ord, String);
};
subclass(Ord, Simple);
/**
* Default ORD instance.
* @type {baja.Ord}
*/
Ord.DEFAULT = new Ord("null");
/**
* Make an ORD.
*
* The argument can be a `String`, `Ord` or an `Object`.
*
* If an `Object` is passed in then if there's a `base` and `child` property,
* this will be used to construct the ORD (by calling `toString` on each).
* Otherwise `toString` will be called on the `Object` for the ORD.
*
* @param {String|baja.Ord|Object} ord
* @returns {baja.Ord}
*
* @example
* <caption>Resolve an ORD</caption>
* baja.Ord.make("station:|slot:/Folder/NumericWritable").get({ lease: true })
* .then(function (numericWritable) {
* baja.outln(numericWritable.getOutDisplay());
* });
*/
Ord.make = function (ord) {
if (arguments.length === 0) {
return Ord.DEFAULT;
}
var ordString;
// Handle child and base
if (typeof ord === "object" && ord.base && ord.child) {
ordString = ord.base.toString() + "|" + ord.child.toString();
} else {
ordString = ord.toString();
}
// Handle URL decoding
if (ordString.match(/^\/ord/)) {
// Remove '/ord?' or '/ord/'
ordString = ordString.substring(5, ordString.length);
// Replace this with the pipe character
ordString = decodeURIComponent(ordString);
}
if (ordString === "" || ordString === "null") {
return Ord.DEFAULT;
}
return new Ord(ordString);
};
/**
* Make an ORD.
*
* @see baja.Ord.make
*
* @param {String|baja.Ord|Object} ord
* @returns {baja.Ord}
*/
Ord.prototype.make = function (ord) {
return Ord.make(ord);
};
/**
* Decode an ORD from a `String`.
*
* @param {String} str the ORD String.
* @returns {baja.Ord} the decoded ORD.
*/
Ord.prototype.decodeFromString = function (str) {
return Ord.make(str);
};
/**
* Encode an ORD to a `String`.
*
* @returns {String} the ORD encoded to a String.
*/
Ord.prototype.encodeToString = function () {
return this.$ord;
};
/**
* @returns {boolean} if this represents the null ORD.
* @since Niagara 4.10
*/
Ord.prototype.isNull = function () {
return this === Ord.DEFAULT;
};
/**
* Return an `String` representation of the object.
*
* @returns {String} a String representation of an ORD.
*/
Ord.prototype.toString = function () {
return this.$ord;
};
/**
* Return the inner value of this `Object`.
*
* @returns {String} a String representation of an ORD.
*/
Ord.prototype.valueOf = function () {
return this.toString();
};
/**
* Parse an ORD to a number of ORD Query objects.
*
* @returns {baja.OrdQueryList} a list of ORDs to resolve.
*/
Ord.prototype.parse = function () {
// TODO: Validate all characters are valid
var os = this.$ord.split("|"), // ORDs
list = new baja.OrdQueryList(),
i,
ind,
schemeName,
scheme,
body;
if (this.$ord === "null") {
return list;
}
for (i = 0; i < os.length; ++i) {
ind = os[i].indexOf(":");
if (ind === -1) {
throw new Error("Unable to parse ORD: " + os[i]);
}
schemeName = os[i].substring(0, ind);
body = os[i].substring(ind + 1, os[i].length);
scheme = baja.OrdScheme.lookup(schemeName);
// Create the ORD scheme
list.add(scheme.parse(schemeName, body));
}
return list;
};
/**
* Resolve an ORD.
*
* Resolving an ORD consists of parsing and processing it to get a result.
* The result is an ORD Target.
*
* Any network calls that result from processing an ORD are always
* asynchronous.
*
* The `resolve` method requires an `ok` function callback or an object
* literal that contains the method's arguments.
*
* Please note that unlike other methods that require network calls, no
* batch object can be specified!
*
* @see module:baja/ord/OrdTarget
* @see baja.Ord#get
* @see baja.RelTime
*
* @param {Object} [obj] the object literal that contains the method's
* arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok function called
* once the ORD has been successfully resolved. The ORD Target is passed to
* this function when invoked.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail function
* called if the ORD fails to resolve. An error cause is passed to this
* function when invoked.
* @param [obj.base] the base Object to resolve the ORD against.
* @param {Boolean} [obj.lease] if defined and true, any Components are
* temporarily subscribed.
* @param {Number|baja.RelTime} [obj.leaseTime] the amount of time in
* milliseconds to lease for (`lease` argument must be true). As well as a
* Number, this can also be a {@link baja.RelTime}. If undefined, BajaScript's
* default lease time will be used.
* @param {baja.Subscriber} [obj.subscriber] if defined the `Component` is
* subscribed using this `Subscriber`.
* @param {Object} [obj.cursor] if defined, this specifies parameters for
* iterating through a Cursor (providing the ORD resolves to a Collection
* or Table). For more information, please see
* {@link baja.coll.tableMixIn.cursor}.
* @returns {Promise.<Object>} a promise that will be resolved with an
* OrdTarget when the ORD has been resolved.
*
* @example
* <caption>Resolve an ORD</caption>
* baja.Ord.make("station:|slot:/").resolve({
* lease: true // ensure any resolved Components are leased
* })
* .then(function (target) {
* // process the ORD Target
* })
* .catch(function (err) {
* // ORD failed to resolve
* });
*/
Ord.prototype.resolve = function (obj) {
var inpObj = obj;
obj = objectify(obj, 'ok');
var inpBase = obj.base,
baseIsNavNode = baja.hasType(inpBase, 'baja:INavNode'),
baseOrd = baseIsNavNode ? inpBase.getNavOrd() : getDefaultBaseOrd(),
base = bajaDef(inpBase, baja.nav.localhost),
cb = obj.cb === undefined ? new Callback(obj.ok, obj.fail) : obj.cb,
subscriber = bajaDef(obj.subscriber, null),
lease = bajaDef(obj.lease, false),
leaseTime = obj.leaseTime, // If undefined, lease will use lease default
full = bajaDef(obj.full, false),
cursor = obj.cursor,
ordQueries,
target,
options,
newTarget;
if (inpBase && !baseIsNavNode) {
cb.fail(new Error('Base must be a NavNode'));
return cb.promise();
}
if (typeof inpObj === "function" || arguments.length === 0) {
obj.lease = true;
}
// Ensure 'this' in callback is the target's Component. If it's not a Component then
// fallback to the resolved Object.
cb.addOk(function (ok, fail, target) {
var resolvedObj = target.getComponent();
if (!resolvedObj) {
resolvedObj = target.getObject();
}
if (resolvedObj !== null) {
ok.call(resolvedObj, target);
} else {
ok(target);
}
});
if (subscriber !== null) {
// If we need to subscribe using a Subscriber once the Component is resolved...
cb.addOk(function (ok, fail, target) {
function newOk() {
ok(target);
}
var comp = target.getComponent();
if (comp !== null && comp.isMounted()) {
subscriber.subscribe({
"comps": [ comp ],
"ok": newOk,
"fail": fail
});
} else {
newOk();
}
});
}
if (lease) {
// If we need to lease once the Component is resolved...
cb.addOk(function (ok, fail, target) {
function newOk() {
ok(target);
}
var comp = target.getComponent();
if (comp !== null && comp.isMounted()) {
comp.lease({
"ok": newOk,
"fail": fail,
"time": leaseTime,
"importAsync": true
});
} else {
newOk();
}
});
}
try {
// Check the user isn't trying to batch an ORD as this isn't supported
if (obj.batch) {
return failCallback(cb, "Cannot batch ORD resolution");
}
ordQueries = this.parse();
if (ordQueries.isEmpty()) {
return failCallback(cb, "Cannot resolve null ORD: " + this.toString());
}
target = new OrdTarget();
target.object = base;
target.ord = this;
options = {
"full": full,
"callback": cb,
"queries": ordQueries,
"ord": this,
"cursor": cursor
};
// Normalize
ordQueries.normalize();
// If there are ORD Schemes that aren't implemented in BajaScript then we
// simply make a network call and resolve the ORD Server side
if (!ordQueries.isClientResolvable() || obj.forceServerResolve) {
newTarget = new OrdTarget(target);
var updateTargetObject;
cb.addOk(function (ok, fail, resp) {
baja.bson.importUnknownTypes(resp, function () {
if (resp.o) {
// Decode the result
var t = newTarget.object = baja.bson.decodeValue(resp.o, baja.$serverDecodeContext);
updateTargetObject = true;
// If we've got a collection result cached then call 'cursor'.
if (resp.c &&
baja.hasType(t) &&
typeof t.cursor === "function") {
cursor.$data = resp.c;
t.cursor(cursor);
}
}
// since Niagara 4.10
// All requests to the OrdChannel will get a SlotPath and the space as
// response if the target is already mounted. Resolving the SlotPath
// will get us the expected mounted component thus reducing any additional
// network calls to get the target objects with all slots loaded.
// If the resolved object and the component are not the same we ensure both
// getComponent and getObject resolve to the expected values.
if (resp && resp.sp) {
var resolveParams = {
ok: function (target) {
if (updateTargetObject) {
var updatedTarget = new OrdTarget(target);
updatedTarget.object = newTarget.object;
ok(updatedTarget);
} else {
ok(target);
}
},
fail: fail,
lease: bajaDef(obj.lease, false),
leaseTime: obj && obj.leaseTime,
subscriber: bajaDef(obj.subscriber, null),
full: bajaDef(obj.full, false)
};
var ordStr = resp.cs + "|slot:" + resp.sp;
if (resp.pp) {
ordStr += '|slot:' + resp.pp.join('/');
}
Ord.make(ordStr).resolve(resolveParams);
} else {
// Finished iterating so just make the callback
ok(newTarget);
}
}, fail);
});
// If Cursor information is defined, ensure we set some defaults
if (options.cursor) {
options.cursor.limit = options.cursor.limit || 10;
options.cursor.offset = options.cursor.offset || 0;
}
// Make the network call to resolve the complete ORD Server side
baja.comm.resolve(this, baseOrd, cb, options);
} else {
// Resolve the ORD. Each ORD scheme must call 'resolveNext' on the cursor to process the next
// part of the ORD. This design has been chosen because some ORD schemes may need to make network calls.
// If the network call is asynchronous in nature then they'll be a delay before the ORD can process further
ordQueries.getCursor().resolveNext(target, options);
}
} catch (err) {
return failCallback(cb, err);
}
return cb.promise();
};
/**
* Resolve the ORD and get the resolved Object from the ORD Target.
*
* This method calls {@link baja.Ord#resolve} and calls `get` on the ORD
* Target to pass the object onto the `ok` function callback.
*
* For more information on how to use this method please see
* {@link baja.Ord#resolve}.
*
* @see module:baja/ord/OrdTarget#resolve
*
* @param {Object} [obj]
* @returns {Promise} a promise that will be resolved with the value specified
* by the ORD.
* @example
* <caption>Resolve/get an ORD</caption>
* baja.Ord.make("service:baja:UserService|slot:jack").get({ lease: true })
* .then(function (user) {
* baja.outln(user.get('fullName'));
* })
* .catch(function (err) {
* baja.error('ORD failed to resolve: ' + err);
* });
*/
Ord.prototype.get = function (obj) {
var oldObj = obj;
obj = objectify(obj, "ok");
if (typeof oldObj === "function" || arguments.length === 0) {
obj.lease = true;
}
obj.cb = new Callback(obj.ok, obj.fail);
obj.cb.addOk(function (ok, fail, target) {
ok.call(this, target.getObject());
});
this.resolve(obj);
return obj.cb.promise();
};
/**
* Return a normalized version of the ORD.
*
* @returns {baja.Ord}
*/
Ord.prototype.normalize = function () {
return Ord.make(this.parse().normalize());
};
/**
* Relativize is used to extract the relative portion
* of this ord within an session:
*
* 1. First the ord is normalized.
* 2. Starting from the left to right, if any queries are
* found which return true for `isSession()`, then remove
* everything from that query to the left.
*
* @see baja.OrdQuery#isSession
* @returns {baja.Ord}
*/
Ord.prototype.relativizeToSession = function () {
var list = this.parse().normalize(),
newList = new baja.OrdQueryList();
for (var i = 0, len = list.size(); i < len; ++i) {
var q = list.get(i);
if (!q.isSession() && !q.isHost()) {
newList.add(q);
}
}
return Ord.make(newList);
};
/**
* Slot and file path ord queries may contain "../" to do relative traversal up the tree. If
* there is more than one backup, the ord will contain "/../", which will be replaced by the
* browser within a URL by removing other sections. For example, https://127.0.0.1/a/b/c/d/../e/f
* is converted to https://127.0.0.1/a/b/c/e/f and https://127.0.0.1/a/b/c/d/../../e/f is
* converted to https://127.0.0.1/a/b/c/f. This will result in unintended behavior in subsequent
* ord resolution with that URL. Therefore, all but the last "../" is replaced with {@code
* "<schema>:..|"}. This function replicates the behavior of BOrdUtil#replaceBackups.
*
* @param {baja.Ord} ord ord that is searched for "../" backups
* @returns {baja.Ord} the original ord if no changes are necessary or an updated ord with the
* necessary replacements
* @since Niagara 4.3U1
*/
Ord.replaceBackups = function (ord) {
var queries = ord.parse();
var newQueries = new baja.OrdQueryList();
var remakeOrd = false;
for (var i = 0; i < queries.size(); ++i) {
var query = queries.get(i);
// In BajaScript, only the slot and virtual schemes have a backup depth. The backup depth is
// only a problem if greater than one because then the ord contains one or more "/../".
if (query.getScheme() instanceof baja.SlotScheme && query.getBackupDepth() > 1) {
remakeOrd = true;
// Replace all but one backup with a new slot path with ".." as the body
for (var j = 0; j < query.getBackupDepth() - 1; ++j) {
newQueries.add(query.makeSlotPath('..'));
}
// Remove all the "/.." from the body of the original OrdQuery. For example,
// slot:../../../abc/def becomes slot:../abc/def
var newBody = query.getBody().replace(/\/\.\./g, '');
newQueries.add(query.makeSlotPath(newBody));
} else {
newQueries.add(query);
}
}
if (remakeOrd) {
ord = baja.Ord.make(newQueries);
}
return ord;
};
/**
* Return the ORD as a URI that can be used in a browser.
*
* @returns {String}
*/
Ord.prototype.toUri = function () {
// Handle whether there is a hyperlink to another Station by guessing
// what's available from fox. For example...
// ip:{ipAddress}|:fox{s}|station:|slot:/ -> http{s}://{ipAddress}/ord/station:%7Cslot:/
var ord = this.normalize(),
uri = String(ord),
res = /^ip:([^|]+)\|fox(s)?:.*/.exec(uri),
prefix = res ? ("http" + (res[2] || "") + "://" + res[1]) : "";
// If the ORD isn't already an HTTP(S) ORD then process it.
if (!uri.match(/^http/i)) {
ord = this.relativizeToSession();
ord = Ord.replaceBackups(ord);
uri = encodeURI(String(ord)).replaceAll(/[#;]/g, function (match) {
return encodeURIComponent(match);
});
uri = "/ord/" + uri;
}
return prefix + uri;
};
/**
* Substitute all variables in the ORD from the given variable map.
* @param {baja.Facets|Object} variables a Facets or object literal containing
* variable names and their values
* @returns {baja.Ord} an ORD with the variables substituted in
* @throws {Error} if a variable name is invalid or empty, or if a variable
* declaration is malformed
* @since Niagara 4.10
*/
Ord.prototype.substitute = function (variables) {
if (!baja.hasType(variables, 'baja:Facets')) {
variables = baja.Facets.make(variables || {});
}
return baja.Ord.make(String(this).replace(new RegExp(VARIABLE_REGEX, 'g'), function (match, key) {
validateVariableMatch(match, key);
return variables.get(key, match);
}));
};
/**
* @returns {boolean} true if this ORD has any variables present
* @throws {Error} if a variable name is invalid or empty, or if a variable
* declaration is malformed
* @since Niagara 4.10
*/
Ord.prototype.hasVariables = function () {
var match = String(this).match(VARIABLE_REGEX);
if (match) {
validateVariableMatch(match[0], match[1]);
return true;
} else {
return false;
}
};
/**
* @returns {string[]} an array of all variable names present in this ORD
* @throws {Error} if a variable name is invalid or empty, or if a variable
* declaration is malformed
* @since Niagara 4.10
*/
Ord.prototype.getVariables = function () {
var str = String(this);
var variables = [];
var match;
var regex = new RegExp(VARIABLE_REGEX, 'g');
while ((match = regex.exec(str))) {
validateVariableMatch(match[0], match[1]);
variables.push(match[1]);
}
return variables;
};
/**
* @param {string} match the whole regex match, like `$(foo)`
* @param {string} name the variable name, like `foo`
*/
function validateVariableMatch(match, name) {
if (match[match.length - 1] !== ')') {
throw new Error('Missing closing paren');
}
if (!name) {
throw new Error('Empty variable name');
}
var illegalChar = name.match(/[^A-Za-z0-9]/);
if (illegalChar) {
throw new Error('Illegal character in variable name: \'' + illegalChar[0] + '\'');
}
}
/**
* Return the data type symbol.
*
* @returns {String} the Symbol used for encoding this data type (primarily
* used for facets).
*/
Ord.prototype.getDataTypeSymbol = function () {
return "o";
};
function failCallback(cb, err) {
if (typeof err === 'string') { err = new Error(err); }
cb.fail(err);
return cb.promise();
}
return Ord;
});