baja/ord/SlotScheme.js

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

/**
 * Defines {@link baja.SlotScheme}.
 * @module baja/ord/SlotScheme
 */
define([
  "bajaScript/comm",
  "bajaScript/baja/ord/OrdScheme",
  "bajaScript/baja/ord/OrdTarget",
  "bajaScript/baja/comm/Callback" ], function (
  baja,
  OrdScheme,
  OrdTarget,
  Callback) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      bajaDef = baja.def;
  
  /**
   * Slot ORD Scheme.
   * 
   * This scheme resolves a `SlotPath` to a Niagara `Object`.
   *
   * @see baja.SlotPath
   *
   * @class
   * @alias baja.SlotScheme
   * @extends baja.OrdScheme
   */    
  var SlotScheme = function SlotScheme() {
    callSuper(SlotScheme, this, arguments);
  };
  
  subclass(SlotScheme, OrdScheme); // TODO: Need to extend from SpaceScheme eventually
  
  /**
   * Default Slot ORD Scheme instance.
   * @private
   * @type {baja.SlotScheme}
   */
  SlotScheme.DEFAULT = new SlotScheme();

  // Empty fail function. It doesn't matter is the loads fail as the resolve without the
  // network calls will result in the fail we want
  var emptyFail = function (ignore) {};
  
  //TODO: refactor
  function slotSchemeResolve(scheme, target, query, cursor, options, netcall) {
    var newTarget = new OrdTarget(target),
        object = target.object;
    
    newTarget.slotPath = query;
    
    var slotPath = newTarget.slotPath, // SlotPath
        space = null,     // Space
        container = null; // base container
        
    netcall = bajaDef(netcall, true);
    
    // TODO: May need to make more robust if other types of Slot Paths are added
    var isVirtual = slotPath.constructor !== baja.SlotPath;
    
    // TODO: Property Container support
    
    if (object.getType().is("baja:VirtualGateway") && isVirtual) {
      space = object.getVirtualSpace();
      container = space.getRootComponent();
    } else if (object.getType().is("baja:ComponentSpace")) {
      space = object;
      container = object.getRootComponent();
    } else if (object.getType().isComponent()) {
      space = object.getComponentSpace();
      if (slotPath.isAbsolute()) {
        container = space.getRootComponent();
      } else {
        container = object; 
      }
    } else {
      throw new Error("Slot Scheme Unsupported ORD base: " + object.getType());
    }
    
    // Hack for Virtual Space
    if (isVirtual && slotPath.getBody() === "" && !object.getType().is("baja:VirtualComponent")) {
      newTarget.object = space;
      cursor.resolveNext(newTarget, options);
      return;
    }
    
    // Avoid a network call if the Component Space doesn't support callbacks
    if (netcall) {
      if (space === null) {
        netcall = false;
      } else {
        netcall = space.hasCallbacks();
      }
    }
    
    var value = container,
        nameAtDepth,   
        slot = null, 
        propertyPath = null, 
        depthRequestIndex = -1,
        backupDepth = slotPath.getBackupDepth(),
        k, i, j;
    
    // first walk up using backup 
    for (k = 0; k < backupDepth; ++k) {
      container = container.getType().isComponent() ? container.getParent() : null;
      value = container;
      
      if (value === null) { 
        throw new Error("Cannot walk backup depth: " + object);
      }
    }
                  
    // Walk the SlotPath
    for (i = 0; i < slotPath.depth(); ++i) {
      nameAtDepth = slotPath.nameAt(i);    
      
      if (isVirtual) {
        nameAtDepth = baja.VirtualPath.toSlotPathName(nameAtDepth);
      }
            
      if (value.getType().isComplex()) {          
        slot = value.getSlot(nameAtDepth);
      } else {
        throw new Error("Unable to resolve ORD: " + slotPath);
      }
                 
      if (slot === null) {
        if (netcall) {    
          depthRequestIndex = i;         
          break;
        }
      
        throw new Error("Unresolved ORD - unable to resolve SlotPath");
      }
      
      if (!slot.isProperty()) {
        if (i !== slotPath.depth() - 1) { // Actions and Topics must be at the final depth
          throw new Error("Unresolved ORD - Actions/Topics must be at final depth: " + slotPath); 
        }
        
        newTarget.container = container;
        newTarget.slot = slot;
        
        // Resolve the next part of the ORD and feed this target into it
        cursor.resolveNext(newTarget, options);
        return;
      }
      
      value = value.get(slot);
      
      // If we have a Component without a Handle then it's probably a value from a frozen Property
      // that's completely unloaded. In this case, we need to perform a load for this Component
      if (value.getType().isComponent() && value.$handle === null && space !== null) {
        if (netcall) {    
          depthRequestIndex = i;         
          break;
        }
      
        throw new Error("Unresolved ORD - unable to load handle for Component");
      }
      
      if (propertyPath === null && value.getType().is("baja:IPropertyContainer")) {
        container = value;
      } else {
        if (propertyPath === null) {
          propertyPath = [];
        }
        propertyPath.push(slot);
      }
    }
    
    // Make a network call to resolve the SlotPath
    var slotOrd,
        batch;
        
    if (netcall && depthRequestIndex > -1) {
      batch = new baja.comm.Batch();
       
      // Load ops on Slots that don't exist
      var startIndex;

      if (slotPath.isAbsolute()) {
        slotOrd = "slot:/";
        startIndex = 0;
      } else {
        slotOrd = "slot:";
        startIndex = depthRequestIndex;
      }
      
      var slotPathInfo = [];
      for (j = startIndex; j < slotPath.depth(); ++j) {
        var slotName = slotPath.nameAt(j);
        if (isVirtual) { slotName = baja.VirtualPath.toSlotPathName(slotName); }

        if (j >= depthRequestIndex) {
          slotPathInfo.push({ o: slotOrd, sn: slotName });
        }
        
        if (j > startIndex) { slotOrd += "/"; }
        slotOrd += slotName;
      }
                 
      var newOk = function () {
        // Now the network calls have all been committed, resolve this
        // Slot path without making any network calls
        scheme.resolve(target, query, cursor, options, /*network call*/false);
      };
      
      var newFail = function (err) {
        options.callback.fail(err);
      };
      
      // Attempt to load the missing Slot Path information
      space.getCallbacks().loadSlotPath(slotPathInfo,
                                        container,      
                                        new Callback(newOk, newFail, batch));
      
      batch.commit();
      return;
    }
    
    if (slot === null && slotPath.depth() > 0) {
      throw new Error("Unable to resolve ORD: " + slotPath);
    }
    
    if (propertyPath === null) {
      newTarget.object = container;
    } else {
      // If there was a Property Path then use the first Property in the Property Path as the Slot
      slot = propertyPath[0];    
      newTarget.container = container; 
      newTarget.object = value;
      newTarget.slot = slot; 
      newTarget.propertyPath = propertyPath;
    }
    
    // Resolve the next part of the ORD and feed this target into it
    cursor.resolveNext(newTarget, options);
  }
  
  function slotSchemeResolveFull(scheme, target, query, cursor, options, netcall) {
    
    var newTarget = new OrdTarget(target),
        object = target.object;
    
    newTarget.slotPath = query;
    
    var slotPath = newTarget.slotPath, // SlotPath
        space = null,     // Space
        container = null, // base container
        isVirtual = slotPath.constructor !== baja.SlotPath;
        
    netcall = bajaDef(netcall, true);
        
    // TODO: Property Container support
    
    if (object.getType().is("baja:VirtualGateway") && isVirtual) {
      space = object.getVirtualSpace();
      container = space.getRootComponent();
    } else if (object.getType().is("baja:ComponentSpace")) {
      space = object;
      container = object.getRootComponent();
    } else if (object.getType().isComponent()) {
      space = object.getComponentSpace();
      if (slotPath.isAbsolute()) {
        container = space.getRootComponent();
      } else {
        container = object; 
      }
    } else {
      throw new Error("Slot Scheme Unsupported ORD base: " + object.getType());
    }
    
    // Hack for Virtual Space
    if (isVirtual && slotPath.getBody() === "" && !object.getType().is("baja:VirtualComponent")) {
      newTarget.object = space;
      cursor.resolveNext(newTarget, options);
      return;
    }
    
    // Avoid a network call if the Component Space doesn't support callbacks
    if (netcall) {
      if (space === null) {
        netcall = false;
      } else {
        netcall = space.hasCallbacks();
      }
    }
    
    var value = container, 
        nameAtDepth,    
        slot = null, 
        propertyPath = null,
        batch = new baja.comm.Batch(),
        depthRequestIndex = 0,
        backupDepth = slotPath.getBackupDepth(),
        k, i, j;
    
    // first walk up using backup 
    for (k = 0; k < backupDepth; ++k) {
      container = container.getType().isComponent() ? container.getParent() : null;
      value = container;
      
      if (value === null) { 
        throw new Error("Cannot walk backup depth: " + object);
      }
    }
            
    // Attempt to load slots on the container if needed
    if (container.getType().isComplex()) {
      container.loadSlots({
        "ok": baja.ok, 
        "fail": emptyFail, 
        "batch": batch
      });
    }
        
    if (batch.isEmpty()) {
      // Walk the SlotPath
      for (i = 0; i < slotPath.depth(); ++i) {
        nameAtDepth = slotPath.nameAt(i); 

        if (isVirtual) {
          nameAtDepth = baja.VirtualPath.toSlotPathName(nameAtDepth);
        }        
              
        if (value.getType().isComplex()) {
          value.loadSlots({
            "ok": baja.ok, 
            "fail": emptyFail, 
            "batch": batch
          });
          
          slot = value.getSlot(nameAtDepth);
        } else {
          throw new Error("Unable to resolve ORD: " + slotPath);
        }
             
        if (netcall && !batch.isEmpty()) {    
          depthRequestIndex = i;         
          break;
        }
        
        if (slot === null) {
          throw new Error("Unresolved ORD - unable to resolve SlotPath");
        }
        
        if (!slot.isProperty()) {
          if (i !== slotPath.depth() - 1) { // Actions and Topics must be at the final depth
            throw new Error("Unresolved ORD - Actions/Topics must be at final depth: " + slotPath); 
          }
          
          newTarget.container = container;
          newTarget.slot = slot;
          
          // Resolve the next part of the ORD and feed this target into it
          cursor.resolveNext(newTarget, options);
          return;
        }
        
        value = value.get(slot);
        
        // If we have a Component without a Handle then it's probably a value from a frozen Property
        // that's completely unloaded. In this case, we need to perform a load for this Component
        if (value.getType().isComponent() && value.$handle === null && space !== null) {
          if (netcall) {    
            depthRequestIndex = i;         
            break;
          }
      
          throw new Error("Unresolved ORD - unable to load handle for Component");
        }
        
        if (propertyPath === null && value.getType().is("baja:IPropertyContainer")) {
          container = value;
        } else {
          if (propertyPath === null) {
            propertyPath = [];
          }
          propertyPath.push(slot);
        }
      }
    }

    // Make a network call to resolve the SlotPath
    var slotOrd;
    if (!batch.isEmpty() && netcall) {
        
      // Load ops on Slots that don't exist
      slotOrd = "slot:";
      if (slotPath.isAbsolute()) {
        slotOrd += "/";
      }
      for (j = 0; j < slotPath.depth(); ++j) {        
        if (j > 0) {
          slotOrd += "/";
        }        
        slotOrd += isVirtual ? baja.VirtualPath.toSlotPathName(slotPath.nameAt(j)) : slotPath.nameAt(j);
        
        if (j >= depthRequestIndex) {
          space.getCallbacks().loadSlots(slotOrd, 0, new Callback(baja.ok, emptyFail, batch));
        }
      }
      
      var newOk = function () {
        // Now the network calls have all been committed, resolve this
        // Slot path without making any network calls
        scheme.resolve(target, query, cursor, options, false);
      };
      
      var newFail = function (err) {
        options.callback.fail(err);
      };
            
      // Finally perform a poll so all of the sync ops can be processed from all the subsequent load ops
      baja.comm.poll(new Callback(newOk, newFail, batch));
      
      batch.commit();
      return;
    }
    
    if (slot === null && slotPath.depth() > 0) {
      throw new Error("Unable to resolve ORD: " + slotPath);
    }
    
    if (propertyPath === null) {
      newTarget.object = container;
    } else {
      // If there was a Property Path then use the first Property in the Property Path as the Slot
      slot = propertyPath[0];    
      newTarget.container = container; 
      newTarget.object = value;
      newTarget.slot = slot; 
      newTarget.propertyPath = propertyPath;
    }
    
    // Resolve the next part of the ORD and feed this target into it
    cursor.resolveNext(newTarget, options);
  }
     
  /**
   * 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.
   * @param {Boolean} [netcall] optional boolean to specify whether a network 
   * call should be attempted (used internally).
   */
  SlotScheme.prototype.resolve = function (target, query, cursor, options, netcall) {
    var that = this,
        cb = options.callback;
    
    function ok() {
      try {
        if (options.full) {
          slotSchemeResolveFull(that, target, query, cursor, options, netcall);
        } else {
          slotSchemeResolve(that, target, query, cursor, options, netcall);
        }
      } catch (err) {
        cb.fail(err);
      }
    }
    
    // Load Slots on any Virtual Gateway before resolving the Slot Path. This will 
    // make sure any Virtual Spaces are fully loaded before we try to resolve and access them.
    if (target.object && target.object.getType().is("baja:VirtualGateway")) {
      target.object.loadSlots({
        ok: ok,
        fail: function (err) {
          cb.fail(err);
        }
      });
    } else {
      ok();
    }
  };
    
  /**
   * Return an ORD Query for the scheme.
   *
   * @returns {baja.SlotPath}
   */
  SlotScheme.prototype.parse = function (schemeName, body) {
    return new baja.SlotPath(body);
  };       
  
  return SlotScheme;
});