baja/ord/ordUtil.js

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

define([
  'bajaScript/sys',
  'bajaPromises' ], function (
  baja,
  Promise) {
  
  "use strict";

  /**
   * API Status: **Private**
   *
   * Utilities for ords.
   *
   * @exports baja/ord/ordUtil
   * @private
   */
  var exports = {};

  /**
   *
   * @param {baja.OrdQueryList} list
   * @param {Number} index
   * @return {boolean}
   */
  exports.trimToStart = function (list, index) {
    if (index > 0) {
      var i;
      for (i = 0; i < index; ++i) {
        list.remove(0);
      }
      return true;
    }
    return false;
  };

  /**
   * This function will return a promise if its able to reuse the `existingOrdTarget` based on the provided parameters.
   * Any subscriber passed to the resolveParams will use subscription if the component is already available.
   * If this function is unable to reuse the OrdTarget, then undefined will be returned to indicate that the caller needs to resolve the ord themselves.
   * Quick resolve will return undefined for one of the following conditions:
   * - the resolveParams.existingOrdTarget is missing.
   * - the resolveParams.existingOrdTarget.getOrd() is different than the data ord.
   * - the result of resolveParams.existingOrdTarget.getComponent() is not yet subscribed.
   * - a resolveParams.cursor is requested.
   *
   * @see module:baja/ord/Ord#resolve
   * @param {*|String|baja.Ord} data Specifies some data used to resolve.
   * @param {Object} [resolveParams] An Object Literal used for ORD resolution.
   * @param {module:baja/ord/OrdTarget} [resolveParams.existingOrdTarget] Use this parameter for an existing OrdTarget that might match the data ord.
   * @param {Function} [resolveParams.ok]  (Deprecated: use Promise) the ok function called once the ORD has been successfully resolved.
   * The OrdTarget is passed to this function when invoked.
   * @param {baja.Subscriber} [resolveParams.subscriber] if defined the `Component` is subscribed using this `Subscriber`.
   * @param {Object} [resolveParams.cursor] if defined, this specifies parameters for
   * iterating through a Cursor.
   * @return {Promise.<module:baja/ord/OrdTarget>|undefined}
   */
  exports.quickResolve = function (data, resolveParams) {
    resolveParams = resolveParams || {};
    var existingOrdTarget = resolveParams.existingOrdTarget;

    var cursor = resolveParams.cursor;
    var ok = resolveParams.ok;
    if (cursor) {
      return;
    }

    var dataString = String(data);
    if (existingOrdTarget !== undefined) {
      if (dataString === String(existingOrdTarget.getOrd())) {

        var subscribePromise;
        var subscriber = resolveParams.subscriber;
        var comp = existingOrdTarget.getComponent();

        //if the previous ordTarget's component is not subscribed, then get a fresh copy
        if (comp && !comp.isSubscribed()) {
          return;
        }

        if (subscriber && comp) {
          subscribePromise = subscriber.subscribe(comp);
        }

        return Promise.resolve(subscribePromise)
          .then(function () {
            return typeof ok === "function" && ok(existingOrdTarget);
          })
          .then(function () {
            return existingOrdTarget;
          });
      }
    }
  };


  /**
   * Resolves ORDs while also calculating ORDs that is optimized for
   * re-resolution in the future. Use this when resolving ORDs that you *know*
   * may be re-resolved again, especially if the original ORDs may include
   * expensive operations like BQL queries.
   *
   * The optimized ORD will be available as an `optimizedOrd` facet on each
   * resolved target.
   *
   * @param {object} params
   * @param {Array.<baja.Ord>} params.ords ords to resolve
   * @param {object} [cx] context to be used for optimizing the ords
   * @returns {Promise.<baja.BatchResolve>}
   * @example
   * return ordUtil.resolveOptimized({
   *   ords: [ station:|slot:/|bql:select * from baja:Component|single:' ],
   *   base: baja.localhost
   * })
   *   .then(function (br) {
   *     var ordTarget = br.getTarget(0);
   *     var optimizedOrd = ordTarget.getFacets().get('optimizedOrd');
   *     // sock it away for re-resolution in the future
   *   });
   */
  exports.resolveOptimized = function (params, cx) {
    var ords = params.ords.map(baja.Ord.make);
    var br = new baja.BatchResolve(ords);
    return br.resolve({ subscriber: params.subscriber })
      .catch(function (ignore) {})
      .then(function () {
        for (var i = 0, len = br.size(); i < len; ++i) {
          if (br.isResolved(i)) {
            var ordTarget = br.getTarget(i);
            ordTarget.$facets = baja.Facets.make(ordTarget.getFacets(), {
              optimizedOrd: exports.optimizeForReresolution(ords[i], ordTarget, cx)
            });
          }
        }
        return br;
      });
  };

  /**
   * @param {baja.Ord} ord
   * @param {module:baja/ord/OrdTarget} ordTarget
   * @param {object} cx
   * @returns {baja.Ord} a substitute ord optimized for reresolution. If the
   * input ord does not contain any substitutable schemes, the input ord will be
   * returned directly.
   */
  exports.optimizeForReresolution = function (ord, ordTarget, cx) {
    var subScheme = exports.findSubstitutableOrdScheme(ord);
    if (subScheme) {
      return subScheme.convertToSubstituteOrd(ord, ordTarget, cx);
    } else {
      return ord;
    }
  };

  /**
   * @param {baja.Ord} ord
   * @returns {baja.OrdScheme|null} the first substitutable ord scheme found in
   * the ord
   * @since Niagara 4.10
   */
  exports.findSubstitutableOrdScheme = function (ord) {
    var queryList = ord.parse();
    for (var i = 0, len = queryList.size(); i < len; i++) {
      var scheme = queryList.get(i).getScheme();
      if (isSubstitutableScheme(scheme)) { return scheme; }
    }
    return null;
  };

  /**
   * Provides the default substitute ORD conversion which is valid if the given
   * OrdTarget result resolves to a mounted {@link baja.Component}, or a slot
   * under mounted Component. Otherwise the original ORD is returned.
   *
   * @param {baja.Ord} ord The original ORD that contains a
   * {@link baja.OrdQuery} using an OrdScheme that is this
   * `baja:ISubstitutableOrdScheme`'s type. This method should not be called if
   * no `baja:ISubstitutableOrdScheme` is present (use
   * `findSubstitutableOrdScheme` first).
   * @param {module:baja/ord/OrdTarget} ordTarget The OrdTarget that the
   * original ORD resolved to which can be useful for computing a more efficient
   * substitute ORD that resolves to this same result.
   * @param {object} cx Additional Context information (for possible future
   * use).
   * @return {baja.Ord} A more efficient substitute ORD that can be used on
   * subsequent ORD resolutions instead of the original ORD to resolve to the
   * same result, or the original ORD if a better substitute ORD can't be found.
   * @since Niagara 4.10
   */
  exports.convertToSubstituteComponentOrd = function (ord, ordTarget, cx) {
    /*
     * devs: when making changes to this logic, please ensure the
     * convertToSubstituteComponentOrd method in `BISubstitutableOrdScheme`
     * receives corresponding logic changes
     */
    try {
      var component = ordTarget && ordTarget.getComponent();
      if (!component) { return ord; }

      // The preferred substitute ORD is either the absolute (handle) ORD for
      // normal station components or the nav (virtual) ORD for virtuals
      var substituteOrd =
        baja.hasType(component, 'baja:VirtualComponent') ?
          component.getNavOrd() : getAbsoluteOrd(component);

      if (!substituteOrd) {
        // Conversion can't happen if the component is not mounted in a
        // component space, so just return the original ORD in that case
        return ord;
      }

      // Check to see if the ORD resolves to a non-BComponent slot under a
      // component, and if so, append that slot's path to the preferred
      // substitute ORD for the parent component
      var propertyPath = ordTarget.propertyPath;
      if (!propertyPath) {
        // Check for an action or topic slot
        var slot = ordTarget.slot;
        if (slot) { propertyPath = [ slot ]; }
      }

      if (propertyPath && propertyPath.length) {
        substituteOrd = baja.Ord.make({
          base: substituteOrd,
          child: 'slot:' + propertyPath.join('/')
        });
      }

      // If there was a view query at the end, also append that to the
      // preferred substitute ORD
      var viewQuery = getViewQuery(ord);
      if (viewQuery) {
        substituteOrd = baja.Ord.make({ base: substituteOrd, child: viewQuery });
      }

      // Almost done, before returning the preferred substitute ORD, we may need
      // to relativize it to the host or session.  So let's interrogate the
      // original ORD to see whether it was already relativized to host/session,
      // and use that input to decide whether to also relativize the substitute
      // result
      var originalNormalizedOrd = ord.normalize();
      var originalOrdStr = String(originalNormalizedOrd);

      // not implementing relativizeToHost here because BajaScript only operates with one host

      // If relativizing the original ORD to session has no effect, then it must
      // have already been relativized to the session
      if (String(originalNormalizedOrd.relativizeToSession()) === originalOrdStr) {
        substituteOrd = substituteOrd.relativizeToSession();
      }

      return substituteOrd;
    } catch (e) {
      // It seems unlikely that exceptions will occur in this method, but just
      // in case, let's log it and return the original ORD
      baja.error(e);
    }

    // If unconvertable, return the original ORD
    return ord;
  };

  /**
   * Normalize and split the ORD into two parts,
   * an ORD without a ViewQuery and the ViewQuery
   * @param {baja.Ord|String} ord
   * @returns {{ord: baja.Ord, viewQuery: baja.ViewQuery}}
   * @since Niagara 4.11
   */
  exports.getOrdViewQuerySplit = function (ord) {
    ord = baja.Ord.make(ord);
    ord = ord.normalize();

    var queryList = ord.parse();
    var last = queryList.get(queryList.size() - 1);
    if (last && last.getSchemeName() === 'view') {
      queryList.remove(queryList.size() - 1);
      return {
        ord: baja.Ord.make(queryList.toString()),
        viewQuery: last
      };
    }
    return { ord: ord };
  };

  /**
   * Append ViewQuery with the provided OrdTarget.
   * Note that the ViewQuery will also be appended to the `optimizedOrd` facet.
   *
   * @param {module:baja/ord/OrdTarget} ordTarget
   * @param {baja.ViewQuery} [viewQuery]
   * @since Niagara 4.11
   */
  exports.appendViewQueryToOrdTarget = function (ordTarget, viewQuery) {
    if (!viewQuery || !ordTarget.ord) {
      return;
    }

    var viewQueryStr = "|view:" + viewQuery.getBody();
    ordTarget.ord = baja.Ord.make(ordTarget.ord + viewQueryStr);
    var facets = ordTarget.$facets;
    if (facets) {
      var optimizedOrd = facets.get('optimizedOrd');
      if (optimizedOrd) {
        optimizedOrd = baja.Ord.make(optimizedOrd + viewQueryStr);
        ordTarget.$facets = baja.Facets.make(facets, baja.Facets.make([ 'optimizedOrd' ], [ optimizedOrd ]));
      }
    }
  };

  function getAbsoluteOrd(component) {
    return baja.Ord.make({
      base: component.getComponentSpace().getAbsoluteOrd(),
      child: getOrdInSpace(component)
    });
  }

  function getHandleOrd(component) {
    return baja.Ord.make('h:' + component.getHandle());
  }

  function getOrdInSpace(component) {
    return getHandleOrd(component);
  }

  function isSubstitutableScheme(scheme) {
    return baja.hasType(scheme, 'baja:ISubstitutableOrdScheme') &&
      typeof scheme.convertToSubstituteOrd === 'function';
  }

  function getViewQuery(ord) {
    var queryList = ord.parse();
    var last = queryList.get(queryList.size() - 1);
    if (last && last.getSchemeName() === 'view') { return last; }
  }
  
  return exports;
});