baja/obj/PermissionsMap.js

/**
 * @copyright 2018 Tridium, Inc. All Rights Reserved.
 * @author Andy Sutton
 */

/**
 * Defines {@link baja.PermissionsMap}.
 * @module baja/obj/PermissionsMap
 */
define([ 'bajaScript/sys',
        'bajaScript/baja/obj/Simple',
        'bajaScript/baja/obj/objUtil' ],
        function (baja, Simple, objUtil) {

  'use strict';

  var subclass = baja.subclass,
      callSuper = baja.callSuper,

      cacheDecode = objUtil.cacheDecode,
      cacheEncode = objUtil.cacheEncode;

  /**
   * PermissionsMap for a given array of baja.Permissions.
   *
   * @class
   * @alias baja.PermissionsMap
   * @extends baja.Simple
   */
  var PermissionsMap = function PermissionsMap(permissions, stringEncoding) {
    callSuper(PermissionsMap, this, arguments);
    this.$permissions = permissions;
    this.$stringEncoding = stringEncoding;  // this variable is called 'string' in BPermissionsMap.java
    this.$size = -1;                        // cache for size
  };

  subclass(PermissionsMap, Simple);

  var permissionsMapDefault = new PermissionsMap([], ''),
      permissionsSuperUser = new PermissionsMap([], 'super');

  /**
   * The SUPER_USER instance automatically is granted
   * all permissions in any category.
   */
  PermissionsMap.SUPER_USER = permissionsSuperUser;

  /**
   * The default instance is the empty permissions map.
   */
  PermissionsMap.DEFAULT = permissionsMapDefault;

  /**
   * Make a PermissionsMap object.
   * Make map where category number maps to array index.
   * The first item (permissions[0]) is unused.  If any
   * items in the array are null, then baja.Permissions.none
   * is assumed.
   *
   * @param {Array.<baja.Permissions>} permissions an array of baja.Permissions to decode.
   * @returns {baja.PermissionsMap}
   */
  PermissionsMap.prototype.make = function (permissions) {
    if (!Array.isArray(permissions) || permissions.length <= 1) {
      return PermissionsMap.DEFAULT;
    }

    // Use a copy of the baja.Permissions array to make a new PermissionsMap.
    // baja.Permissions objects are Simples and should be immutable so a shallow copy should be fine.
    return new PermissionsMap(permissions.slice(), null);
  };

  /**
   * Make a PermissionsMap object.
   *
   * @param {Array.<baja.Permissions>} permissions an array of baja.Permissions to decode.
   * @returns {baja.PermissionsMap}
   */
  PermissionsMap.make = function (permissions) {
    return PermissionsMap.DEFAULT.make.apply(baja.PermissionsMap.DEFAULT, arguments);
  };

  /**
   * Return if this is the super user permission map which
   * is automatically granted all permissions in all categories.
   */
  PermissionsMap.prototype.isSuperUser = function () {
    return this === PermissionsMap.SUPER_USER;
  };

  /**
   * Get the number of category/permissions mappings.
   */
  PermissionsMap.prototype.size = function () {
    if (this.$size < 0) {
      var p, x = this.$permissions.length - 1;
      while (x > 0) {
        p = this.$permissions[x];
        if (p && p.getMask() !== 0) { break; }
        x--;
      }
      this.$size = x + 1;
    }

    return this.$size;
  };

  /**
   * Given a category index, return the mapped baja.Permissions
   * or return baja.Permissions.none if no mapping configured.
   * If this is the super user, then always return baja.Permissions.all.
   *
   * @param {Number} index
   * @returns {baja.Permissions}
   * @throws {Error} If index is not a number or less than 1
   */
  PermissionsMap.prototype.getPermissions = function (index) {
    var p, categoryIndex;

    if (index) { categoryIndex = Number(index); }
    if (index === 0) { categoryIndex = 0; }

    if (!categoryIndex && categoryIndex !== 0) {
      throw new Error('invalid Category index: ' + index);
    }

    // no such thing as category 0 (or below)
    if (categoryIndex < 1) { throw new Error('Category index < 1'); }

    // super is always all
    if (this === PermissionsMap.SUPER_USER) { return baja.Permissions.all; }

    // out of range is always none
    if (categoryIndex >= this.$permissions.length) { return baja.Permissions.none; }

    // otherwise map to zero
    p = this.$permissions[categoryIndex];

    return p || baja.Permissions.none;
  };

  /**
   * Decode `PermissionsMap` from a `String`.
   *
   * @method
   * @returns {baja.PermissionsMap}
   */
  PermissionsMap.prototype.decodeFromString = cacheDecode(function (s) {
    var permissions = [],
        eq, index, p;

    // special string encoding
    if (s === 'super') { return PermissionsMap.SUPER_USER; }
    if (s === '') { return PermissionsMap.DEFAULT; }

    s.split(';').forEach(function (token) {
      eq = token.indexOf('=');
      index = token.substring(0, eq);

      p = baja.Permissions.make(token.substring(eq + 1));
      // assign into specified index
      permissions[index] = p;
    });

    return PermissionsMap.make(permissions);
  });

  /**
   * Encode `PermissionsMap` to a `String`.
   *
   * @method
   * @returns {String}
   */
  PermissionsMap.prototype.encodeToString = cacheEncode(function () {
    if (typeof this.$stringEncoding !== 'string') {
      var i, p, s = '';

      // encode into 'index=permission;' pairs
      for (i = 1; i < this.$permissions.length; i++) {
        p = this.$permissions[i];
        // don't encode for this index if there's no baja.Permissions, or if it's bajaPermissions.none (ie 0)
        if (!p || !p.getMask()) { continue; }
        if (s.length > 0) { s += ';'; }
        s += i + '=' + p.encodeToString();
      }

      this.$stringEncoding = s;
    }

    return this.$stringEncoding;
  });

  /**
   * Create a new baja.PermissionsMap from the bitwise OR of this baja.PermissionsMap
   * instance and the specified baja.PermissionsMap.
   *
   * @param {baja.PermissionsMap} otherPermissionsMap
   * @returns {baja.PermissionsMap}
   */
  PermissionsMap.prototype.or = function (otherPermissionsMap) {
    if (this.isSuperUser() || otherPermissionsMap.isSuperUser()) {
      return baja.PermissionsMap.SUPER_USER;
    }

    if (this === baja.PermissionsMap.DEFAULT && otherPermissionsMap === baja.PermissionsMap.DEFAULT) {
      return baja.PermissionsMap.DEFAULT;
    }

    var pMap1Size = this.size(),
      pMap2Size = otherPermissionsMap.size(),
      newSize = Math.max(pMap1Size, pMap2Size),
      newPermissions = [];

    for (var i = 1; i < newSize; i++) {
      var p1 = i < pMap1Size ? this.getPermissions(i) : baja.Permissions.none;
      var p2 = i < pMap2Size ? otherPermissionsMap.getPermissions(i) : baja.Permissions.none;

      newPermissions[i] = p1.or(p2);
    }

    return baja.PermissionsMap.make(newPermissions);
  };

  return PermissionsMap;
});