/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* BOX System Object Notation.
*
* BSON is BOG notation in a JSON format. JSON is used instead of XML because
* tests show that browsers can parse JSON significantly faster.
*/
define([
"lex!",
"bajaScript/comp",
"bajaScript/baja/obj/objUtil",
"bajaPromises" ], function defineBson(
lexjs,
baja,
objUtil,
Promise) {
// Use ECMAScript 5 Strict Mode
"use strict";
// Create local for improved minification
const { BaseBajaObj, callSuper, def: bajaDef, objectify, subclass } = baja;
const { capitalizeFirstLetter } = objUtil;
const serverDecodeContext = baja.$serverDecodeContext = { serverDecode: true };
const defaultDecodeAsync = baja.Simple.prototype.decodeAsync;
let bsonDecodeValue;
/**
* BOX System Object Notation.
* @namespace baja.bson
*/
baja.bson = new BaseBajaObj();
////////////////////////////////////////////////////////////////
// Decode Actions/Topics
////////////////////////////////////////////////////////////////
/**
* Decode the BSON for an Action Type.
*
* @private
*
* @param bson the BSON to decode.
* @returns {Type|null} the parameter for the Action Type (or null if none).
*/
function decodeActionParamType(bson) {
const { apt: actionParamType } = bson;
return typeof actionParamType === 'string' ? baja.lt(actionParamType) : null;
}
/**
* Decode the BSON for an Action Return Type.
*
* @private
*
* @param bson the BSON to decode.
* @returns {Type|null} the parameter for the Action Return Type (or null if none).
*/
function decodeActionReturnType(bson) {
const { art: actionReturnType } = bson;
return typeof actionReturnType === 'string' ? baja.lt(actionReturnType) : null;
}
/**
* Decode the BSON for the Topic and return the event type.
*
* @private
*
* @param bson the BSON to decode.
* @returns {Type|null} the event type for a Topic or null if not present
*/
function decodeTopicEventType(bson) {
const { tet: topicEventType } = bson;
return typeof topicEventType === 'string' ? baja.lt(topicEventType) : null;
}
////////////////////////////////////////////////////////////////
// BSON Frozen Slots
////////////////////////////////////////////////////////////////
/**
* Frozen Property Slot.
*
* Property defines a Slot which is a storage location
* for a variable in a Complex.
*
* A new object should never be directly created with this Constructor. All Slots are
* created internally by BajaScript.
*
* @class
* @alias FrozenProperty
* @extends baja.Property
*/
const FrozenProperty = function (bson, complex) {
callSuper(FrozenProperty, this, [ bson.n, bson.dn ]);
this.$bson = bson;
this.$complex = complex;
};
subclass(FrozenProperty, baja.Property);
FrozenProperty.prototype.isFrozen = function () {
return true;
};
/**
* Return the Property value.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @private
*
* @returns value
*/
FrozenProperty.prototype.$getValue = function () {
if (this.$val === undefined) {
const val = this.$val = bsonDecodeValue(this.$bson.v, serverDecodeContext);
// Set up any parenting if needed
if (val.getType().isComplex() && this.$complex) {
val.$parent = this.$complex;
val.$propInParent = this;
}
}
return this.$val;
};
/**
* Return true if the value has been lazily decoded.
*
* @private
*
* @returns {Boolean}
*/
FrozenProperty.prototype.$isValueDecoded = function () {
return this.$val !== undefined;
};
/**
* Set the Property value.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @private
*
* @param val value to be set.
*/
FrozenProperty.prototype.$setValue = function (val) {
this.$val = val;
};
/**
* Return the Flags for the Property.
*
* @see baja.Flags
*
* @returns {Number}
*/
FrozenProperty.prototype.getFlags = function () {
if (this.$flags === undefined) {
this.$flags = decodeFlags(this.$bson);
}
return this.$flags;
};
/**
* Set the Flags for the Property.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @private
* @see baja.Flags
*
* @param {Number} flags
*/
FrozenProperty.prototype.$setFlags = function (flags) {
this.$flags = flags;
};
/**
* Return the Facets for the Property.
*
* @see baja.Facets
*
* @returns {baja.Facets} the Slot Facets
*/
FrozenProperty.prototype.getFacets = function () {
if (this.$facets === undefined) {
this.$facets = this.$bson.x === undefined ? baja.Facets.DEFAULT : baja.Facets.DEFAULT.decodeFromString(this.$bson.x, baja.Simple.$unsafeDecode);
}
return this.$facets;
};
/**
* Set the Facets for the Property.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @private
* @see baja.Facets
*
* @param {baja.Facets} facets
*/
FrozenProperty.prototype.$setFacets = function (facets) {
this.$facets = facets;
};
/**
* Return the default flags for the Property.
*
* @returns {Number}
*/
FrozenProperty.prototype.getDefaultFlags = function () {
if (this.$defFlags === undefined) {
this.$defFlags = decodeFlags(this.$bson);
}
return this.$defFlags;
};
/**
* Return the default value for the Property.
*
* @returns the default value for the Property.
*/
FrozenProperty.prototype.getDefaultValue = function () {
if (this.$defVal === undefined) {
this.$defVal = bsonDecodeValue(this.$bson.v, serverDecodeContext);
}
return this.$defVal;
};
/**
* Return the Type for this Property.
*
* @returns {Type} the Type for the Property.
*/
FrozenProperty.prototype.getType = function () {
if (this.$initType === undefined) {
this.$initType = baja.lt(this.$bson.ts || this.$bson.v.t);
}
return this.$initType || null;
};
/**
* Return the display String for this Property.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @private
*
* @returns {String}
*/
FrozenProperty.prototype.$getDisplay = function () {
if (this.$display === undefined) {
this.$display = this.$bson.v.d || "";
}
return this.$display;
};
/**
* Set the display for this Property.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @private
*
* @param {String} display the display String
*/
FrozenProperty.prototype.$setDisplay = function (display) {
this.$display = display;
};
/**
* Frozen Action Slot.
*
* Action is a Slot that defines a behavior which can
* be invoked on a Component.
*
* A new object should never be directly created with this Constructor. All Slots are
* created internally by BajaScript
*
* @class
* @alias FrozenAction
* @extends baja.Action
*/
const FrozenAction = function (bson) {
callSuper(FrozenAction, this, [ bson.n, bson.dn ]);
this.$bson = bson;
};
subclass(FrozenAction, baja.Action);
FrozenAction.prototype.isFrozen = FrozenProperty.prototype.isFrozen;
/**
* Return the Flags for the Action.
*
* @function
* @see baja.Flags
*
* @returns {Number}
*/
FrozenAction.prototype.getFlags = FrozenProperty.prototype.getFlags;
/**
* Set the Flags for the Action.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @function
* @private
* @see baja.Flags
*
* @param {Number} flags
*/
FrozenAction.prototype.$setFlags = FrozenProperty.prototype.$setFlags;
/**
* Return the Facets for the Action.
*
* @function
* @see baja.Facets
*
* @returns the Slot Facets
*/
FrozenAction.prototype.getFacets = FrozenProperty.prototype.getFacets;
/**
* Set the Facets for the Action.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @function
* @private
* @see baja.Facets
*
* @param {baja.Facets} facets
*/
FrozenAction.prototype.$setFacets = FrozenProperty.prototype.$setFacets;
/**
* Return the default flags for the Action.
*
* @returns {Number}
*/
FrozenAction.prototype.getDefaultFlags = FrozenProperty.prototype.getDefaultFlags;
/**
* Return the Action's Parameter Type.
*
* @returns {Type|null} the Parameter's Type (or null if the Action has no argument).
*/
FrozenAction.prototype.getParamType = function () {
if (this.$paramType === undefined) {
this.$paramType = decodeActionParamType(this.$bson);
}
return this.$paramType;
};
/**
* Previously, this returned the Action's Default Value.
*
* @throws {Error} throws an error, this method is no longer supported.
* @deprecated
*/
FrozenAction.prototype.getParamDefault = function () {
throw new Error('Action parameter default is not available');
};
/**
* Return the return Type for the Action.
*
* @returns {Type|null} the return Type (or null if the Action has no return Type).
*/
FrozenAction.prototype.getReturnType = function () {
if (this.$returnType === undefined) {
this.$returnType = decodeActionReturnType(this.$bson);
}
return this.$returnType;
};
/**
* Frozen Topic Slot.
*
* Topic defines a Slot which indicates an event that
* is fired on a Component.
*
* A new object should never be directly created with this Constructor. All Slots are
* created internally by BajaScript.
*
* @class
* @alias FrozenTopic
* @extends baja.Topic
*/
const FrozenTopic = function (bson) {
callSuper(FrozenTopic, this, [ bson.n, bson.dn ]);
this.$bson = bson;
};
subclass(FrozenTopic, baja.Topic);
FrozenTopic.prototype.isFrozen = FrozenProperty.prototype.isFrozen;
/**
* Return the Flags for the Topic.
*
* @function
* @see baja.Flags
*
* @returns {Number}
*/
FrozenTopic.prototype.getFlags = FrozenProperty.prototype.getFlags;
/**
* Set the Flags for the Topic.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @function
* @private
* @see baja.Flags
*
* @param {Number} flags
*/
FrozenTopic.prototype.$setFlags = FrozenProperty.prototype.$setFlags;
/**
* Return the Facets for the Topic.
*
* @function
* @see baja.Facets
*
* @returns the Slot Facets
*/
FrozenTopic.prototype.getFacets = FrozenProperty.prototype.getFacets;
/**
* Set the Facets for the Topic.
*
* Please note, this method is intended for INTERNAL use by Tridium only. An
* external developer should never call this method.
*
* @function
* @private
* @see baja.Facets
*
* @param {baja.Facets} facets
*/
FrozenTopic.prototype.$setFacets = FrozenProperty.prototype.$setFacets;
/**
* Return the default flags for the Topic.
*
* @returns {Number}
*/
FrozenTopic.prototype.getDefaultFlags = FrozenProperty.prototype.getDefaultFlags;
/**
* Return the event type.
*
* @returns {Type|null} the event type (or null if the Topic has no event).
*/
FrozenTopic.prototype.getEventType = function () {
if (this.$eventType === undefined) {
this.$eventType = decodeTopicEventType(this.$bson);
}
return this.$eventType;
};
////////////////////////////////////////////////////////////////
// Auto-generate Slot Methods
////////////////////////////////////////////////////////////////
function generateSlotMethods(complexType, complex, slot) {
// Cache auto-generated methods onto Type
const autoGen = complexType.$autoGen = complexType.$autoGen || {};
const slotName = slot.getName();
let methods = autoGen.hasOwnProperty(slotName) ? autoGen[slotName] : null;
// If the methods already exist then simply copy them over and return
if (methods) {
for (let methodName in methods) {
if (methods.hasOwnProperty(methodName)) {
complex[methodName] = methods[methodName];
}
}
return;
}
autoGen[slotName] = methods = {};
// Please note: these auto-generated methods should always respect the fact that a Slot can be overridden by
// sub-Types. Therefore, be aware of using too much closure in these auto-generated methods.
// Form appropriate name for getters, setters and firers
const capSlotName = capitalizeFirstLetter(slot.getName());
if (slot.isProperty()) {
const origGetterName = "get" + capSlotName;
let getterName = origGetterName;
const origGetterDisplayName = origGetterName + "Display";
let getterDisplayName = origGetterDisplayName;
const origSetterName = "set" + capSlotName;
let setterName = origSetterName;
// Find some unique names for the getter and setter (providing this isn't the icon Slot which we DO want to override)...
if (capSlotName !== "Icon") {
let i = 0;
while (complex[getterName] !== undefined || complex[getterDisplayName] !== undefined || complex[setterName] !== undefined) {
getterName = origGetterName + (++i);
getterDisplayName = origGetterDisplayName + i;
setterName = origSetterName + i;
}
}
// Add Getter
complex[getterName] = methods[getterName] = function () {
const v = this.get(slotName);
// If a number then return its inner boxed value
return v.getType().isNumber() ? v.valueOf() : v;
};
// Add Display String Getter
complex[getterDisplayName] = methods[getterDisplayName] = function (cx) {
return this.getDisplay(slotName, cx);
};
// Add Setter
complex[setterName] = methods[setterName] = function (obj) {
obj = objectify(obj, "value");
obj.slot = slotName;
// TODO: Need to check incoming value to ensure it's the same Type!!!
return this.set(obj);
};
}
let invokeActionName = slotName;
if (slot.isAction()) {
// Find a unique name for the Action invocation method
let i = 0;
while (complex[invokeActionName] !== undefined) {
invokeActionName = slotName + (++i);
}
complex[invokeActionName] = methods[invokeActionName] = function (obj) {
obj = objectify(obj, "value");
obj.slot = slotName;
return this.invoke(obj);
};
}
if (slot.isTopic()) {
// Find a unique name for the topic invocation method
const origFireTopicName = "fire" + capSlotName;
let fireTopicName = origFireTopicName;
let i = 0;
while (complex[fireTopicName] !== undefined) {
fireTopicName = origFireTopicName + (++i);
}
complex[fireTopicName] = methods[fireTopicName] = function (obj) {
obj = objectify(obj, "value");
obj.slot = slotName;
return this.fire(obj);
};
}
}
////////////////////////////////////////////////////////////////
// Contracts
////////////////////////////////////////////////////////////////
/**
* Return an instance of a frozen Slot.
*
* @param {Object} bson
* @param {baja.Complex} [complex]
*
* @returns {baja.Slot}
*/
function createContractSlot(bson, complex) {
const slotType = bson.st;
// Create frozen Slot
switch (slotType) {
case 'p':
return new FrozenProperty(bson, complex);
case 'a':
return new FrozenAction(bson);
case 't':
return new FrozenTopic(bson);
default:
throw new Error("Invalid BSON: Cannot decode: " + JSON.stringify(bson));
}
}
/**
* Return a decoded Contract Slot.
*
* @private
*
* @param {Type} complexType
* @param {baja.Complex} complex the Complex to decode the Slots onto
* @param {Object} bson the BSON to decode
*/
function decodeContractSlot(complexType, complex, bson) {
const slot = createContractSlot(bson, complex);
const slotName = bson.n;
// Only auto-generate the Slot methods if Slot doesn't already exist.
// This caters for Slots that are overridden by sub-Types.
if (!complex.$map.contains(slotName)) {
// Auto-generate the methods and copy them over to the complex
generateSlotMethods(complexType, complex, slot);
}
// Add to Slot Map
complex.$map.put(slotName, slot);
}
/**
* Return a decoded array of Slots from a BSON Contract Definition.
*
* @private
*
* @see baja.Slot
*
* @param type the Type.
* @param {baja.Complex} complex the complex instance the Slots are being loaded for.
*/
baja.bson.decodeComplexContract = function (type, complex) {
const clxTypes = [];
let t = type;
// Get a list of all the Super types
while (t && t.isComplex()) {
clxTypes.push(t);
t = t.getSuperType();
}
// Iterate down through the Super Types and build up the Contract list
for (let i = clxTypes.length - 1; i >= 0; --i) {
const bson = clxTypes[i].getContract();
if (bson) {
for (let j = 0; j < bson.length; ++j) {
// Add newly created Slot to array
decodeContractSlot(type, complex, bson[j]);
}
}
}
};
////////////////////////////////////////////////////////////////
// BSON Type Scanning
////////////////////////////////////////////////////////////////
/**
* Scan a BSON object for type information. Only types known to the core component model will be
* scanned - this includes Property types, Action/Topic arguments and return values, slot facets,
* and values encoded into other core baja Simples like Status/Facets/Enums. The discovered types
* can then be imported before decoding the values from the BSON, so no type information will be
* missing at runtime.
*
* This will *not* scan for non-core Simples that might reference other Types. Type Extensions
* for Simples can still load their own types when they are instantiated, separate from this
* process, by implementing `decodeAsync`.
*
* @private
*
* @param {Object} bson A chunk of BSON to scan.
* @param {Object} typeSpecs type specs will be appended onto this object as keys.
*/
baja.bson.scan = function (bson, typeSpecs) {
if (!bson) {
return;
}
const { nm } = bson;
// Ensure we're dealing with a Slot, an AddOp or a SetFacetsOp.
if (nm === "p" || nm === "a" || nm === "t" || nm === "a" || nm === "x") {
// If we've found a Type then record it
if (bson.t) {
typeSpecs[bson.t] = bson.t;
}
// Scan any type dependencies for facets
if (bson.xtd) {
for (let i = 0; i < bson.xtd.length; ++i) {
typeSpecs[bson.xtd] = bson.xtd;
}
}
// If this Property specifies some other Type dependencies then pick them up here
if (nm === "p") {
if (bson.td) {
for (let i = 0; i < bson.td.length; ++i) {
typeSpecs[bson.td[i]] = bson.td[i];
}
}
if (bson.s) {
for (let i = 0; i < bson.s.length; ++i) {
baja.bson.scan(bson.s[i], typeSpecs);
}
}
}
// If an Action then record any parameter or return Type
if (nm === "a") {
// Parameter Type
if (bson.apt) {
typeSpecs[bson.apt] = bson.apt;
}
// Return Type
if (bson.art) {
typeSpecs[bson.art] = bson.art;
}
}
// If a Topic then record any event type
if (nm === "t" && bson.tet) {
typeSpecs[bson.tet] = bson.tet;
}
}
// Scan for other Slots
if (bson instanceof Array) {
for (let i = 0; i < bson.length; ++i) {
if (bson[i] && (bson[i] instanceof Array || bson[i] instanceof Object)) {
baja.bson.scan(bson[i], typeSpecs);
}
}
} else if (bson instanceof Object) {
for (let prop in bson) {
if (bson.hasOwnProperty(prop)) {
if (bson[prop] && (bson[prop] instanceof Array || bson[prop] instanceof Object)) {
baja.bson.scan(bson[prop], typeSpecs);
}
}
}
}
};
/**
* Scan for Types and Contracts that aren't yet loaded into the BajaScript Registry.
*
* @private
*
* @param bson the BSON to scan Types for.
* @param {Function} ok the ok callback.
* @param {Function} [fail] the fail callback.
* @param {baja.comm.Batch} [batch] the optional batch.
* @returns {Promise}
*/
baja.bson.importUnknownTypes = function (bson, ok, fail, batch) {
// Store results in an object as we only want type information added once
const typeSpecs = {};
// Scan the data structure for Slot Type information
baja.bson.scan(bson, typeSpecs);
return baja.importTypes({
"typeSpecs": Object.keys(typeSpecs),
"ok": ok,
"fail": fail || baja.fail,
"batch": batch
});
};
const bsonImportUnknownTypes = baja.bson.importUnknownTypes;
////////////////////////////////////////////////////////////////
// BOG BSON Decoding
////////////////////////////////////////////////////////////////
/**
* Decode and return a Knob.
*
* @private
*
* @param {Object} bson the BSON that contains knob information to decode
* @returns {Object} a decoded value (null if unable to decode)
*/
baja.bson.decodeKnob = function (bson) {
const targetOrd = baja.Ord.make(bson.to);
return {
/**
* Get the ID for this knob
* @returns {Number}
*/
getId: function getId() {
return bson.id;
},
/**
* Get the name for the link on the source component
* @since Niagara 4.15
* @returns {String|null}
*/
getLinkName: function getLinkName() {
return bson.ln || null;
},
/**
* Get the source component for the link
* @returns {baja.Component|null}
*/
getSourceComponent: function getSourceComponent() {
return null;
},
/**
* Get the slot on the source component for the link
* @returns {String}
*/
getSourceSlotName: function getSourceSlotName() {
return bson.ss;
},
/**
* Get the target Ord for the link
* @returns {baja.Ord}
*/
getTargetOrd: function getTargetOrd() {
return targetOrd;
},
/**
* Get slot on the target component for the link
* @returns {String}
*/
getTargetSlotName: function getTargetSlotName() {
return bson.ts;
}
};
};
/**
* Decode and return a Relation Knob.
*
* @private
*
* @param {Object} bson the BSON that contains relation knob information to decode
* @returns {Object} a decoded value (null if unable to decode).
*/
baja.bson.decodeRelationKnob = function (bson) {
return {
/**
* Get the ID for this relation knob
* @returns {Number}
*/
getId: function getId() {
return bson.id;
},
/**
* Get the name for the relation on the source component
* @since Niagara 4.15
* @returns {String|null}
*/
getRelationName: function getRelationName() {
return bson.rn || null;
},
/**
* Get the relation ID
* @returns {String}
*/
getRelationId: function getRelationId() {
return bson.ri;
},
/**
* Get the relation tags
* @returns {baja.Facets}
*/
getRelationTags: function getRelationTags() {
return baja.Facets.DEFAULT.decodeFromString(bson.rt, baja.Simple.$unsafeDecode);
},
/**
* Get the relation Ord
* @returns {baja.Ord}
*/
getRelationOrd: function getRelationOrd() {
return baja.Ord.make(bson.ro);
}
};
};
/**
* Return a decoded value.
*
* @private
*
* @param bson the BSON to decode.
* @param {Object} [cx] the context used when decoding.
* @param {baja.Complex} [parent] the parent Complex that will contain the decoded value. Only to
* be used internally!
* @returns {baja.Value|null} a decoded value (null if unable to decode).
*/
baja.bson.decodeValue = function (bson, cx, parent) {
const { nm, s: kidEncodings } = bson;
// TODO: Skip this from LoadOp - needed for loading security permissions at some point!
if (!isValidSlotElementName(nm)) {
return null;
}
cx = cx || {};
const slot = getDecodingSlot(bson, parent);
let value = null;
if (!isActionOrTopic(slot)) {
value = decodeValueSync(bson, slot, cx);
}
saveToParent(bson, value, parent, slot, cx);
if (kidEncodings) {
for (let i = 0; i < kidEncodings.length; ++i) {
bsonDecodeValue(kidEncodings[i], cx, value);
}
}
return value;
};
bsonDecodeValue = baja.bson.decodeValue;
/**
* decodeAsync will resolve to the decoded value. Use this method if you are
* not sure whether all types in the bson have already been imported; it will
* detect and import all unknown types before it attempts to decode the value.
*
* @private
*
* @param bson the BSON to decode.
* @param {Object} [cx] the context used when decoding.
* @param {baja.Complex} [parent]
* @returns {Promise<baja.Value|null>} resolves to a decoded value (null if unable to decode).
*/
baja.bson.decodeAsync = function (bson, cx, parent) {
return bsonImportUnknownTypes(bson)
.then(() => decodeComplexTreeAsync(bson, cx, parent))
.then(([ value, slot ]) => {
saveToParent(bson, value, parent, slot, cx);
return value;
});
};
////////////////////////////////////////////////////////////////
// BSON Encoding
////////////////////////////////////////////////////////////////
/**
* @param {object} bson
* @param {baja.Property} [prop]
* @returns {baja.Value} the default instance of the value represented in the BSON
*/
function instantiateSync(bson, prop) {
const { t: typeSpec } = bson;
// TODO: Should be getDefaultValue()?
return typeSpec === undefined ? prop.$getValue() : baja.$(typeSpec);
}
/**
* If decoding either a standalone BSON blob, or the default value of a frozen Property, we have
* to start with the default instance. Since frozen Properties can have async-decoded default
* values (such as a Facets with an un-imported FrozenEnum range), we have to go down an async
* path to get the default value in both cases.
*
* Please note that this does _not_ add support for async decoding of default values when calling
* baja.$()!
*
* @param {object} bson a BSON blob from the browser
* @param [slot] if decoding a frozen Slot
* @returns {Promise<baja.Value>}
*/
function instantiateAsync(bson, slot) {
const { t: typeSpec } = bson;
const isFrozenProperty = typeSpec === undefined;
// we start with the default instance, no async decoding yet.
const instance = isFrozenProperty ? slot.$getValue() : baja.$(typeSpec);
// next, ensure that any frozen Simples have a chance to decode async.
return decodeFrozenSimpleDefaults(instance)
.then(() => {
// this BSON may be for a Complex, and it may be either standalone or a frozen Complex Property.
// each slot now needs a chance to decode.
// if I'm a standalone value sent from the station, these frozen slot definitions are provided in the BSON itself.
// if I'm a frozen slot, these frozen slot definitions come from my parent Complex.
const { s: slotEncodings } = isFrozenProperty ? slot.$bson.v : bson;
if (!slotEncodings) {
return instance;
} else {
return Promise.all(slotEncodings.map((slotBson, i) => {
const slot = getDecodingSlot(slotBson, instance);
if (!isActionOrTopic(slot)) {
return decodeValueAsync(slotBson, slot)
.then((value) => [ value, slot ]);
} else {
return [ null, slot ];
}
}))
.then((results) => {
results.forEach(([ kidValue, kidSlot ], i) => {
const slotBson = slotEncodings[i];
saveToParent(slotBson, kidValue, instance, kidSlot, serverDecodeContext);
});
return instance;
});
}
});
}
/**
* @param {object} bson a BSON blob from the station
* @param {baja.Property} [prop] if decoding a frozen Property
* @returns {baja.Value} the instance of the value as encoded to string in the BSON. If a Complex,
* there is no string encoding, this is just the default instance of that Complex type.
*/
function decodeValueSync(bson, prop, cx) {
const { v: valueEncoding } = bson;
let obj = instantiateSync(bson, prop);
if (obj.getType().isSimple() && valueEncoding !== undefined) {
obj = obj.decodeFromString(valueEncoding, baja.Simple.$unsafeDecode);
}
applyMetadataFromBson(bson, obj, cx);
return obj;
}
/**
* @param {object} bson a BSON blob from the station.
* @param {baja.Property} [prop] provided if decoding a Property on a Complex
* @param {object} cx
* @returns {Promise.<baja.Value>} the instance of the value as encoded to string in the BSON. If a Complex,
* there is no string encoding, this is just the default instance of that Complex type.
*/
function decodeValueAsync(bson, prop, cx) {
const { v: valueEncoding } = bson;
return instantiateAsync(bson, prop)
.then((defaultInstance) => {
/*
valueEncoding will be undefined for a Complex
*/
if (defaultInstance.getType().isSimple() && valueEncoding !== undefined) {
return defaultInstance.decodeAsync(valueEncoding, baja.Simple.$unsafeDecode);
} else {
return defaultInstance;
}
})
.then((value) => {
applyMetadataFromBson(bson, value, cx);
return value;
});
}
/**
* Decodes a complete tree of BSON, giving any async Simples a chance to decode asynchronously.
* It is expected that unknown types have already been imported before this function is called.
*
* @param {object} bson BSON encoding of a value, and if a Complex, its child slots
* @param {object} cx context
* @param {baja.Complex} [parent] parent complex that will receive the decoded value - this
* function itself will **not** save the decoded value onto this complex
* @returns {Array|Promise.<Array>} resolves to the decoded value (or null if nothing to decode),
* and if it is to be saved to the parent complex, the slot to which it should be saved
*/
function decodeComplexTreeAsync(bson, cx, parent) {
const { nm } = bson;
// TODO: Skip this from LoadOp - needed for loading security permissions at some point!
if (!isValidSlotElementName(nm)) {
return [ null ];
}
cx = cx || {};
const slot = getDecodingSlot(bson, parent);
if (isActionOrTopic(slot)) {
return [ null ];
}
return decodeValueAsync(bson, slot, cx)
.then((value) => [ value, slot ]);
}
/**
* If a frozen slot needs to decode asynchronously, it will cause problems later because frozen
* properties otherwise get synchronously, lazily decoded on demand (see
* FrozenProperty#$getValue). We will look for Simples that need to decode asynchronously and
* decode them now, so `complex.get('frozenSlotThatNeedsToDecodeAsync')` will succeed later.
* @param {baja.Value} value
* @returns {Promise}
*/
function decodeFrozenSimpleDefaults(value) {
if (!value.getType().isComplex()) {
return Promise.resolve();
}
const frozenSimpleProperties = value.getSlots()
.filter((slot) => slot.isProperty() && slot.isFrozen() && slot.getType().isSimple())
.toArray();
return Promise.all(frozenSimpleProperties.map((prop) => {
const ctor = prop.getType().findCtor();
if (ctor === String || ctor === Boolean || ctor === Number) {
return;
}
if (ctor.prototype.decodeAsync === defaultDecodeAsync) {
return;
}
return baja.bson.decodeAsync(prop.$bson.v)
.then((val) => prop.$setValue(val));
}));
}
/**
* @param {object} bson
* @param {baja.Complex} [parent]
* @returns {baja.Slot|null} the slot in the parent Complex this BSON will get decoded into, if
* applicable
*/
function getDecodingSlot(bson, parent) {
const { dn: slotDisplayName, n: slotName, nm: slotType } = bson;
const slot = parent && parent.getSlot(slotName);
if (slot) {
if (slotDisplayName !== undefined) {
slot.$setDisplayName(slotDisplayName);
}
} else {
if (slotType !== "p") {
throw new Error("Error decoding Slot from BSON: Missing frozen Slot: " + slotType);
}
}
return slot;
}
function isActionOrTopic(slot) {
return slot && !slot.isProperty();
}
function isValidSlotElementName(nm) {
return nm === 'p' || nm === 'a' || nm === 't';
}
/**
* Finishes applying any additional data encoded in the BSON to the value instance we are decoding
* in the browser.
*
* @param {object} bson
* @param {baja.Value} value
* @param {object} cx
*/
function applyMetadataFromBson(bson, value, cx) {
const {
d: displayValue,
h: handle,
l: loadInfoEncoding,
nc: isNavChild,
nk: knobEncodings,
nrk: relationKnobEncodings,
stub
} = bson;
const type = value.getType();
if (displayValue !== undefined && value instanceof baja.DefaultSimple) {
value.$displayValue = displayValue;
}
// Decode BSON specifically for baja:Action and baja:Topic
if (type.isAction()) {
value.$paramType = decodeActionParamType(bson);
value.$returnType = decodeActionReturnType(bson);
} else if (type.isTopic()) {
value.$eventType = decodeTopicEventType(bson);
}
// Decode Component
if (type.isComponent()) {
if (handle !== undefined) {
value.$handle = handle;
}
if (isNavChild !== undefined) {
value.$nc = isNavChild === "true";
}
if (knobEncodings) {
for (let i = 0; i < knobEncodings.length; ++i) {
value.$fw("installKnob", baja.bson.decodeKnob(knobEncodings[i]), cx);
}
}
if (relationKnobEncodings) {
for (let i = 0; i < relationKnobEncodings.length; ++i) {
value.$fw("installRelationKnob", baja.bson.decodeRelationKnob(relationKnobEncodings[i]), cx);
}
}
if (loadInfoEncoding) {
const { p: permissionsEncoding } = loadInfoEncoding;
if (typeof permissionsEncoding === "string") {
value.$fw("setPermissions", permissionsEncoding);
}
}
// TODO: Handle Component Stub decoding here
if (!stub) {
value.$bPropsLoaded = true;
}
}
}
/**
* Saves the bson and decoded value (if provided) to the parent Complex. This runs synchronously and will not make
* network calls.
*
* @param {object} bson
* @param {baja.Value} [value]
* @param {baja.Complex} [parent]
* @param {baja.Slot} [slot]
* @param {object} cx
*/
function saveToParent(bson, value, parent, slot, cx) {
if (!parent) {
return;
}
const { d: displayString, dn: displayName, f: flagEncoding } = bson;
const hasFlags = flagEncoding !== undefined;
const flags = hasFlags && decodeFlags(bson);
const valueProvided = value !== null && value !== undefined;
try {
if (displayName !== undefined) {
cx.displayName = displayName;
}
if (displayString !== undefined) {
cx.display = displayString;
}
if (slot) {
if (hasFlags && flags !== slot.getFlags()) {
parent.setFlags({ slot, flags, cx });
}
if (valueProvided) {
parent.set({ slot, value: value, cx });
}
} else if (parent.getType().isComponent()) {
const { n: slotName, x: facetsEncoding } = bson;
const facets = baja.Facets.DEFAULT.decodeFromString(bajaDef(facetsEncoding, ""), baja.Simple.$unsafeDecode);
const flags = decodeFlags(bson);
if (valueProvided) {
parent.add({ slot: slotName, value: value, flags, facets, cx });
}
}
} finally {
cx.displayName = undefined;
cx.display = undefined;
}
}
/**
* From a bson object, derive the slot flags as a number. If the flags were
* incorrectly encoded as a number `bson.flags`, use that - otherwise use the
* correct method of decoding `bson.f` from string. See NCCB-25981.
* @param {object} bson
* @returns {number} slot flags, or 0 if not present
*/
function decodeFlags(bson) {
const flags = bson.flags;
if (typeof flags === 'number') { return flags; }
return baja.Flags.decodeFromString(bajaDef(bson.f, "0"));
}
function encodeSlot(parObj, par, slot) {
if (slot === null) {
return;
}
// Encode Slot Flags (if they differ from the default
let value = null; // Property Value
let skipv = false; // Skip value
if (slot.isProperty()) {
value = par.get(slot);
if (slot.isFrozen()) {
if (value.equivalent(slot.getDefaultValue())) {
skipv = true;
}
}
} else {
skipv = true;
}
const flags = par.getFlags(slot);
// Skip frozen Slots that have default flags and value
if (flags === slot.getDefaultFlags() && skipv) {
return;
}
// Encode Slot Type
const o = {};
if (slot.isProperty()) {
o.nm = "p";
} else if (slot.isAction()) {
o.nm = "a";
} else if (slot.isTopic()) {
o.nm = "t";
}
// Slot name
o.n = slot.getName();
// Slot Flags if necessary
if (((!slot.isFrozen() && flags !== 0) || (flags !== slot.getDefaultFlags())) && par.getType().isComponent()) {
o.f = baja.Flags.encodeToString(flags);
}
// Slot facets if necessary
const fc = slot.getFacets();
if (!slot.isFrozen() && fc.getKeys().length > 0) {
o.x = fc.encodeToString();
}
if (value !== null && value.getType().isComponent()) {
// Encode handle
if (value.isMounted()) {
o.h = value.getHandle();
}
// TODO: Categories and stub?
}
// TODO: Need to re-evalulate this method by going through BogEncoder.encodeSlot again
if (!skipv && slot.isProperty()) {
o.t = value.getType().getTypeSpec();
encodeVal(o, value);
}
// Now we've encoded the Slot, add it to the Slots array
if (!parObj.s) {
parObj.s = [];
}
parObj.s.push(o);
}
function encodeVal(obj, val) {
const valueType = val.getType();
if (valueType.isSimple()) {
const defaultInstance = valueType.getInstance();
//Throw if blacklisted (see BlacklistedTypes) and trying to set a different value
if (valueType.$isBlackListed && !val.equals(defaultInstance)) {
throw new Error("Cannot add blacklisted types to a component");
}
if (valueType.$isSensitive && !baja.bson.$canEncodeSecurely()) {
throw new Error(lexjs.$getSync({
module: 'baja',
key: 'password.encoder.secureEnvironmentRequired',
def: 'Cannot encode sensitive values to string in an insecure environment.'
}));
}
// only send string encoding if not the DEFAULT instance
if (val !== defaultInstance) {
// Encode Simple
obj.v = val.encodeToString();
}
} else {
// Encode Complex
const cursor = val.getSlots();
// Encode all the Slots on the Complex
while (cursor.next()) {
encodeSlot(obj, val, cursor.get());
}
}
}
function encode(name, val) {
const o = { nm: "p" };
// Encode name
if (name !== null) {
o.n = name;
}
if (val.getType().isComponent()) {
// Encode handle
if (val.isMounted()) {
o.h = val.getHandle();
}
// TODO: Encode categories
// TODO: Encode whether this Component is fully loaded or not???
}
o.t = val.getType().getTypeSpec();
encodeVal(o, val);
return o;
}
/**
* Return an encoded BSON value.
*
* @private
*
* @param val the value to encode to BSON.
* @returns encoded BSON value.
*/
baja.bson.encodeValue = function (val) {
return encode(null, val);
};
/**
* @private
* @returns {boolean}
*/
baja.bson.$canEncodeSecurely = function () {
return baja.comm.$isSecure() || baja.isOffline() || !baja.$requireHttps;
};
return baja;
});