baja/ord/HierarchyScheme.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.HierarchyScheme}.
 * @module baja/ord/HierarchyScheme
 */
define([
  'bajaPromises',
  "bajaScript/sys",
  "bajaScript/baja/ord/Ord",
  "bajaScript/baja/ord/OrdCoalescer",
  "bajaScript/baja/ord/OrdQuery",
  "bajaScript/baja/ord/OrdScheme",
  "bajaScript/baja/ord/OrdTarget",
  "bajaScript/baja/ord/SlotPath",
  "bajaScript/baja/ord/ordUtil" ], function (
    Promise,
    baja,
    Ord,
    OrdCoalescer,
    OrdQuery,
    OrdScheme,
    OrdTarget,
    SlotPath,
    ordUtil) {

  "use strict";

  var subclass = baja.subclass,
    callSuper = baja.callSuper,
    trimToStart = ordUtil.trimToStart,
    hierarchySpaceTypeSpec = "hierarchy:HierarchySpace",
    hierarchyLevelElemTypeSpec = "hierarchy:LevelElem",
    virtualComponentTypeSpec = "baja:VirtualComponent";

  /**
   * Before resolving any hierarchy: ORD, the hierarchy space must be resolved
   * and associated type specs must be loaded. In turn, this will lazily resolve
   * the BajaScript library for managing hierarchies from the hierarchy module
   * (HierarchySpace Type Extension).
   * @function
   */
  var ensureSpaceResolved = (function () {
    var prom;
    return function () {
      return prom || (prom = baja.Ord.make("hierarchy:")
        .resolve({ forceServerResolve: true })
        .then(function () {
          return baja.importTypes({
            typeSpecs: [
              hierarchySpaceTypeSpec,
              hierarchyLevelElemTypeSpec,
              virtualComponentTypeSpec
            ]
          });
        }));
    };
  }());

  // will coalesce all duplicate hierarchy: ord requests
  var coalescer = new OrdCoalescer({ delay: 0 });
  coalescer.$batchResolve = function (params) {
    // hierarchy ords need forceServerResolve but BatchResolve doesn't support
    // this, so resolve them one by one. it's the same amount of network calls
    // either way.
    var ords = params.ords;
    var subscriber = params.subscriber;
    return Promise.all(ords.map(function (ord) {
      return baja.Ord.make(ord).resolve({
        forceServerResolve: true,
        subscriber: subscriber
      })
        .catch(function (err) { return err; });
    }));
  };

  /**
   * Hierarchy ORD Scheme.
   *
   * This ORD scheme is used for resolving Niagara 4 hierarchies. The hierarchy
   * module must be installed on the Station in order for this to successfully
   * resolve.
   *
   * @class
   * @alias baja.HierarchyScheme
   * @extends baja.OrdScheme
   * @private
   */
  var HierarchyScheme = function HierarchyScheme() {
    callSuper(HierarchyScheme, this, arguments);
  };

  subclass(HierarchyScheme, OrdScheme);

  /**
   * Default Handle ORD Scheme instance
   * @private
   * @type {baja.HierarchyScheme}
   */
  HierarchyScheme.DEFAULT = new HierarchyScheme();

  /**
   * Called when an ORD is resolved.
   *
   * @private
   *
   * @see baja.OrdScheme#resolve
   *
   * @param {module:baja/ord/OrdTarget} target  the current ORD Target.
   * @param {baja.OrdQuery} query  the ORD Query used in resolving the ORD.
   * @param {module:baja/ord/OrdQueryListCursor} cursor  the ORD Query List
   * cursor used for helping to asynchronously resolve the ORD.
   * @param {Object} options  options used for resolving an ORD.
   */
  HierarchyScheme.prototype.resolve = function (target, query, cursor, options) {
    ensureSpaceResolved()
      .then(function () {
        resolveHierarchy(target, query, cursor, options);
      })
      .catch(function (err) {
        return options.callback.fail(err);
      });
  };

  function resolveHierarchy(target, query, cursor, options) {
    // Create the hierarchy space node if it does not exist already
    if (!baja.nav.localhost.hierarchy) {
      baja.nav.localhost.hierarchy = baja.nav.localhost.$addChildNode(
        baja.$(hierarchySpaceTypeSpec));
    }

    var newTarget = new OrdTarget(target);

    if (!query.getBody()) {
      newTarget.object = baja.nav.localhost.hierarchy;
      cursor.resolveNext(newTarget, options);
    } else {
      // We used to short-circuit if the last item in the query body started with "station:|".  This
      // would, however, resolve a component even if the user did not have permission to view the
      // hierarchy listed at the beginning of the query body.  We did not want to add another
      // network call just to check the user's permissions on a particular hierarchy.  Also, we did
      // not want to start caching hierarchy space nav children at this time.  That would possibly
      // prevent a network call: if the hierarchy is in the nav children, go ahead and
      // short-circuit.  But, refreshing this nav child cache would have to be considered.

      // Resolve the ORD and force it to resolve on the Server.
      coalescer.resolve(query.toString())
        .then(function (target) {
          newTarget = new OrdTarget(target);
          newTarget.object = target.object;
          cursor.resolveNext(newTarget, options);
        })
        .catch(function (err) {
          options.callback.fail(err);
        });
    }
  }

  /**
   * Return an ORD Query for the scheme.
   *
   * @returns {baja.OrdQuery}
   */
  HierarchyScheme.prototype.parse = function (schemeName, body) {
    return hierarchyQuery(this, schemeName, body);
  };

  function hierarchyQuery(scheme, schemeName, body) {
    return new OrdQuery({
      scheme: scheme,
      schemeName: schemeName,
      body: body,
      isHost: false,
      isSession: false,
      normalize: function (list, index) {
        var modified;
        if (list.isSameScheme(index, index + 1)) {
          var sp1 = new SlotPath(body);
          var sp2 = new SlotPath(list.get(index + 1).getBody());
          list.merge(index, hierarchyQuery(scheme, schemeName, sp1.merge(sp2)));
          modified = true;
        }
        return trimToStart(list, index) || modified;
      }
    });
  }

  return HierarchyScheme;
});