<commit_before>ngGridServices.factory('$sortService', ['$parse', function($parse) {
    var sortService = {};
    sortService.colSortFnCache = {}; // cache of sorting functions. Once we create them, we don't want to keep re-doing it
    // this takes an piece of data from the cell and tries to determine its type and what sorting
    // function to use for it
    // @item - the cell data
    sortService.guessSortFn = function(item) {
        var itemType = typeof(item);
        //check for numbers and booleans
        switch (itemType) {
            case "number":
                return sortService.sortNumber;
            case "boolean":
                return sortService.sortBool;
            case "string":
                // if number string return number string sort fn. else return the str
                return item.match(/^-?[£$¤]?[\d,.]+%?$/) ? sortService.sortNumberStr : sortService.sortAlpha;
            default:
                //check if the item is a valid Date
                if (Object.prototype.toString.call(item) === '[object Date]') {
                    return sortService.sortDate;
                } else {
                    //finally just sort the basic sort...
                    return sortService.basicSort;
                }
        }
    };
    //#region Sorting Functions
    sortService.basicSort = function(a, b) {
        if (a == b) {
            return 0;
        }
        if (a < b) {
            return -1;
        }
        return 1;
    };
    sortService.sortNumber = function(a, b) {
        return a - b;
    };
    sortService.sortNumberStr = function(a, b) {
        var numA, numB, badA = false, badB = false;
        numA = parseFloat(a.replace(/[^0-9.-]/g, ''));
        if (isNaN(numA)) {
            badA = true;
        }
        numB = parseFloat(b.replace(/[^0-9.-]/g, ''));
        if (isNaN(numB)) {
            badB = true;
        }
        // we want bad ones to get pushed to the bottom... which effectively is "greater than"
        if (badA && badB) {
            return 0;
        }
        if (badA) {
            return 1;
        }
        if (badB) {
            return -1;
        }
        return numA - numB;
    };
    sortService.sortAlpha = function(a, b) {
        var strA = a.toLowerCase(),
            strB = b.toLowerCase();
        return strA == strB ? 0 : (strA < strB ? -1 : 1);
    };
    sortService.sortDate = function(a, b) {
        var timeA = a.getTime(),
            timeB = b.getTime();
        return timeA == timeB ? 0 : (timeA < timeB ? -1 : 1);
    };
    sortService.sortBool = function(a, b) {
        if (a && b) {
            return 0;
        }
        if (!a && !b) {
            return 0;
        } else {
            return a ? 1 : -1;
        }
    };
    //#endregion
    // the core sorting logic trigger
    sortService.sortData = function(sortInfo, data /*datasource*/) {
        // first make sure we are even supposed to do work
        if (!data || !sortInfo) {
            return;
        }
        var l = sortInfo.fields.length,
            order = sortInfo.fields,
            col,
            direction,
            // IE9 HACK.... omg, I can't reference data array within the sort fn below. has to be a separate reference....!!!!
            d = data.slice(0);
        //now actually sort the data
        data.sort(function (itemA, itemB) {
            var tem = 0,
                indx = 0,
                sortFn;
            while (tem == 0 && indx < l) {
                // grab the metadata for the rest of the logic
                col = sortInfo.columns[indx];
                direction = sortInfo.directions[indx],
                sortFn = sortService.getSortFn(col, d);

                var propA = $parse(order[indx])(itemA);
                var propB = $parse(order[indx])(itemB);
                // we want to allow zero values to be evaluated in the sort function
                if ((!propA && propA != 0) || (!propB && propB != 0)) {
                    // we want to force nulls and such to the bottom when we sort... which effectively is "greater than"
                    if (!propB && !propA) {
                        tem = 0;
                    } else if (!propA) {
                        tem = 1;
                    } else if (!propB) {
                        tem = -1;
                    }
                } else {
                    tem = sortFn(propA, propB);
                }
                indx++;
            }
            //made it this far, we don't have to worry about null & undefined
            if (direction === ASC) {
                return tem;
            } else {
                return 0 - tem;
            }
        });
    };
    sortService.Sort = function(sortInfo, data) {
        if (sortService.isSorting) {
            return;
        }
        sortService.isSorting = true;
        sortService.sortData(sortInfo, data);
        sortService.isSorting = false;
    };
    sortService.getSortFn = function(col, data) {
        var sortFn = undefined, item;
        //see if we already figured out what to use to sort the column
        if (sortService.colSortFnCache[col.field]) {
            sortFn = sortService.colSortFnCache[col.field];
        } else if (col.sortingAlgorithm != undefined) {
            sortFn = col.sortingAlgorithm;
            sortService.colSortFnCache[col.field] = col.sortingAlgorithm;
        } else { // try and guess what sort function to use
            item = data[0];
            if (!item) {
                return sortFn;
            }
            sortFn = sortService.guessSortFn($parse(col.field)(item));
            //cache it
            if (sortFn) {
                sortService.colSortFnCache[col.field] = sortFn;
            } else {
                // we assign the alpha sort because anything that is null/undefined will never get passed to
                // the actual sorting function. It will get caught in our null check and returned to be sorted
                // down to the bottom
                sortFn = sortService.sortAlpha;
            }
        }
        return sortFn;
    };
    return sortService;
}]);
<commit_msg>allow optional '+' prefix to trigger numerical sort<commit_after>ngGridServices.factory('$sortService', ['$parse', function($parse) {
    var sortService = {};
    sortService.colSortFnCache = {}; // cache of sorting functions. Once we create them, we don't want to keep re-doing it
    // this takes an piece of data from the cell and tries to determine its type and what sorting
    // function to use for it
    // @item - the cell data
    sortService.guessSortFn = function(item) {
        var itemType = typeof(item);
        //check for numbers and booleans
        switch (itemType) {
            case "number":
                return sortService.sortNumber;
            case "boolean":
                return sortService.sortBool;
            case "string":
                // if number string return number string sort fn. else return the str
                return item.match(/^[-+]?[£$¤]?[\d,.]+%?$/) ? sortService.sortNumberStr : sortService.sortAlpha;
            default:
                //check if the item is a valid Date
                if (Object.prototype.toString.call(item) === '[object Date]') {
                    return sortService.sortDate;
                } else {
                    //finally just sort the basic sort...
                    return sortService.basicSort;
                }
        }
    };
    //#region Sorting Functions
    sortService.basicSort = function(a, b) {
        if (a == b) {
            return 0;
        }
        if (a < b) {
            return -1;
        }
        return 1;
    };
    sortService.sortNumber = function(a, b) {
        return a - b;
    };
    sortService.sortNumberStr = function(a, b) {
        var numA, numB, badA = false, badB = false;
        numA = parseFloat(a.replace(/[^0-9.-]/g, ''));
        if (isNaN(numA)) {
            badA = true;
        }
        numB = parseFloat(b.replace(/[^0-9.-]/g, ''));
        if (isNaN(numB)) {
            badB = true;
        }
        // we want bad ones to get pushed to the bottom... which effectively is "greater than"
        if (badA && badB) {
            return 0;
        }
        if (badA) {
            return 1;
        }
        if (badB) {
            return -1;
        }
        return numA - numB;
    };
    sortService.sortAlpha = function(a, b) {
        var strA = a.toLowerCase(),
            strB = b.toLowerCase();
        return strA == strB ? 0 : (strA < strB ? -1 : 1);
    };
    sortService.sortDate = function(a, b) {
        var timeA = a.getTime(),
            timeB = b.getTime();
        return timeA == timeB ? 0 : (timeA < timeB ? -1 : 1);
    };
    sortService.sortBool = function(a, b) {
        if (a && b) {
            return 0;
        }
        if (!a && !b) {
            return 0;
        } else {
            return a ? 1 : -1;
        }
    };
    //#endregion
    // the core sorting logic trigger
    sortService.sortData = function(sortInfo, data /*datasource*/) {
        // first make sure we are even supposed to do work
        if (!data || !sortInfo) {
            return;
        }
        var l = sortInfo.fields.length,
            order = sortInfo.fields,
            col,
            direction,
            // IE9 HACK.... omg, I can't reference data array within the sort fn below. has to be a separate reference....!!!!
            d = data.slice(0);
        //now actually sort the data
        data.sort(function (itemA, itemB) {
            var tem = 0,
                indx = 0,
                sortFn;
            while (tem == 0 && indx < l) {
                // grab the metadata for the rest of the logic
                col = sortInfo.columns[indx];
                direction = sortInfo.directions[indx],
                sortFn = sortService.getSortFn(col, d);

                var propA = $parse(order[indx])(itemA);
                var propB = $parse(order[indx])(itemB);
                // we want to allow zero values to be evaluated in the sort function
                if ((!propA && propA != 0) || (!propB && propB != 0)) {
                    // we want to force nulls and such to the bottom when we sort... which effectively is "greater than"
                    if (!propB && !propA) {
                        tem = 0;
                    } else if (!propA) {
                        tem = 1;
                    } else if (!propB) {
                        tem = -1;
                    }
                } else {
                    tem = sortFn(propA, propB);
                }
                indx++;
            }
            //made it this far, we don't have to worry about null & undefined
            if (direction === ASC) {
                return tem;
            } else {
                return 0 - tem;
            }
        });
    };
    sortService.Sort = function(sortInfo, data) {
        if (sortService.isSorting) {
            return;
        }
        sortService.isSorting = true;
        sortService.sortData(sortInfo, data);
        sortService.isSorting = false;
    };
    sortService.getSortFn = function(col, data) {
        var sortFn = undefined, item;
        //see if we already figured out what to use to sort the column
        if (sortService.colSortFnCache[col.field]) {
            sortFn = sortService.colSortFnCache[col.field];
        } else if (col.sortingAlgorithm != undefined) {
            sortFn = col.sortingAlgorithm;
            sortService.colSortFnCache[col.field] = col.sortingAlgorithm;
        } else { // try and guess what sort function to use
            item = data[0];
            if (!item) {
                return sortFn;
            }
            sortFn = sortService.guessSortFn($parse(col.field)(item));
            //cache it
            if (sortFn) {
                sortService.colSortFnCache[col.field] = sortFn;
            } else {
                // we assign the alpha sort because anything that is null/undefined will never get passed to
                // the actual sorting function. It will get caught in our null check and returned to be sorted
                // down to the bottom
                sortFn = sortService.sortAlpha;
            }
        }
        return sortFn;
    };
    return sortService;
}]);
<|endoftext|>


<commit_before>/**
 * Created by Arthur Sudre on 21/07/16.
 * Copyright © 2016 Xebia IT Architects. All rights reserved.
 */

import request from 'request-promise';
import { live } from "../config";

export default class LiveService {

  sendWebhook(user) {
    const options = {
      method: 'POST',
      uri: live.host + live.usersUpdateEndpoint,
      json: true,
      headers: {
        'Authorization': live.basicAuthorization
      },
      body: user,
      resolveWithFullResponse: true
    };
    return request(options);
  };

}
<commit_msg>[Live] change basic authorization env variable (removed 'Basic ')<commit_after>/**
 * Created by Arthur Sudre on 21/07/16.
 * Copyright © 2016 Xebia IT Architects. All rights reserved.
 */

import request from 'request-promise';
import { live } from "../config";

export default class LiveService {

  sendWebhook(user) {
    const options = {
      method: 'POST',
      uri: live.host + live.usersUpdateEndpoint,
      json: true,
      headers: {
        'Authorization': 'Basic ' + live.basicAuthorization
      },
      body: user,
      resolveWithFullResponse: true
    };
    return request(options);
  };

}
<|endoftext|>


<commit_before>(function(global) {

  'use strict';

  var fabric = global.fabric || (global.fabric = {}),
      clone  = fabric.util.object.clone;

  /**
   * Textbox class, based on IText, allows the user to resize the text rectangle
   * and wraps lines automatically. Textboxes have their Y scaling locked, the
   * user can only change width. Height is adjusted automatically based on the
   * wrapping of lines.
   * @class fabric.Textbox
   * @extends fabric.IText
   * @mixes fabric.Observable
   * @return {fabric.Textbox} thisArg
   * @see {@link fabric.Textbox#initialize} for constructor definition
   */
  fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {

    /**
     * Type of an object
     * @type String
     * @default
     */
    type: 'textbox',

    /**
     * Minimum width of textbox, in pixels.
     * @type Number
     * @default
     */
    minWidth: 20,

    /**
     * Minimum calculated width of a textbox, in pixels.
     * @type Number
     * @default
     */
    dynamicMinWidth: 0,

    /**
     * Cached array of text wrapping.
     * @type Array
     */
    __cachedLines: null,

    /**
     * Override standard Object class values
     */
    lockScalingY: true,

    /**
     * Override standard Object class values
     */
    lockScalingFlip: true,

    /**
     * Constructor. Some scaling related property values are forced. Visibility
     * of controls is also fixed; only the rotation and width controls are
     * made available.
     * @param {String} text Text string
     * @param {Object} [options] Options object
     * @return {fabric.Textbox} thisArg
     */
    initialize: function(text, options) {
      this.ctx = fabric.util.createCanvasElement().getContext('2d');
      this.callSuper('initialize', text, options);
      this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility());

      // add width to this list of props that effect line wrapping.
      this._dimensionAffectingProps.width = true;
    },

    /**
     * Unlike superclass's version of this function, Textbox does not update
     * its width.
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @private
     * @override
     */
    _initDimensions: function(ctx) {
      if (this.__skipDimension) {
        return;
      }

      if (!ctx) {
        ctx = fabric.util.createCanvasElement().getContext('2d');
        this._setTextStyles(ctx);
      }

      // clear dynamicMinWidth as it will be different after we re-wrap line
      this.dynamicMinWidth = 0;

      // wrap lines
      this._textLines = this._splitTextIntoLines();

      // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
      if (this.dynamicMinWidth > this.width) {
        this._set('width', this.dynamicMinWidth);
      }

      // clear cache and re-calculate height
      this._clearCache();
      this.height = this._getTextHeight(ctx);
    },

    /**
     * Generate an object that translates the style object so that it is
     * broken up by visual lines (new lines and automatic wrapping).
     * The original text styles object is broken up by actual lines (new lines only),
     * which is only sufficient for Text / IText
     * @private
     */
    _generateStyleMap: function() {
      var realLineCount     = 0,
          realLineCharCount = 0,
          charCount         = 0,
          map               = {};

      for (var i = 0; i < this._textLines.length; i++) {
        if (this.text[charCount] === '\n') {
          realLineCharCount = 0;
          charCount++;
          realLineCount++;
        }
        else if (this.text[charCount] === ' ') {
          // this case deals with space's that are removed from end of lines when wrapping
          realLineCharCount++;
          charCount++;
        }

        map[i] = { line: realLineCount, offset: realLineCharCount };

        charCount += this._textLines[i].length;
        realLineCharCount += this._textLines[i].length;
      }

      return map;
    },

    /**
     * @param {Number} lineIndex
     * @param {Number} charIndex
     * @param {Boolean} [returnCloneOrEmpty=false]
     * @private
     */
    _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) {
      if (this._styleMap) {
        var map = this._styleMap[lineIndex];
        if (!map) {
          return returnCloneOrEmpty ? { } : null;
        }
        lineIndex = map.line;
        charIndex = map.offset + charIndex;
      }
      return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty);
    },

    /**
     * @param {Number} lineIndex
     * @param {Number} charIndex
     * @param {Object} style
     * @private
     */
    _setStyleDeclaration: function(lineIndex, charIndex, style) {
      var map = this._styleMap[lineIndex];
      lineIndex = map.line;
      charIndex = map.offset + charIndex;

      this.styles[lineIndex][charIndex] = style;
    },

    /**
     * @param {Number} lineIndex
     * @param {Number} charIndex
     * @private
     */
    _deleteStyleDeclaration: function(lineIndex, charIndex) {
      var map = this._styleMap[lineIndex];
      lineIndex = map.line;
      charIndex = map.offset + charIndex;

      delete this.styles[lineIndex][charIndex];
    },

    /**
     * @param {Number} lineIndex
     * @private
     */
    _getLineStyle: function(lineIndex) {
      var map = this._styleMap[lineIndex];
      return this.styles[map.line];
    },

    /**
     * @param {Number} lineIndex
     * @param {Object} style
     * @private
     */
    _setLineStyle: function(lineIndex, style) {
      var map = this._styleMap[lineIndex];
      this.styles[map.line] = style;
    },

    /**
     * @param {Number} lineIndex
     * @private
     */
    _deleteLineStyle: function(lineIndex) {
      var map = this._styleMap[lineIndex];
      delete this.styles[map.line];
    },

    /**
     * Wraps text using the 'width' property of Textbox. First this function
     * splits text on newlines, so we preserve newlines entered by the user.
     * Then it wraps each line using the width of the Textbox by calling
     * _wrapLine().
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @param {String} text The string of text that is split into lines
     * @returns {Array} Array of lines
     */
    _wrapText: function(ctx, text) {
      var lines = text.split(this._reNewline), wrapped = [], i;

      for (i = 0; i < lines.length; i++) {
        wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i));
      }

      return wrapped;
    },

    /**
     * Helper function to measure a string of text, given its lineIndex and charIndex offset
     *
     * @param {CanvasRenderingContext2D} ctx
     * @param {String} text
     * @param {number} lineIndex
     * @param {number} charOffset
     * @returns {number}
     * @private
     */
    _measureText: function(ctx, text, lineIndex, charOffset) {
      var width = 0;
      charOffset = charOffset || 0;

      for (var i = 0, len = text.length; i < len; i++) {
        width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset);
      }

      return width;
    },

    /**
     * Wraps a line of text using the width of the Textbox and a context.
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @param {String} text The string of text to split into lines
     * @param {Number} lineIndex
     * @returns {Array} Array of line(s) into which the given text is wrapped
     * to.
     */
    _wrapLine: function(ctx, text, lineIndex) {
      var lineWidth        = 0,
          lines            = [],
          line             = '',
          words            = text.split(' '),
          word             = '',
          offset           = 0,
          infix            = ' ',
          wordWidth        = 0,
          infixWidth       = 0,
          largestWordWidth = 0,
          lineJustStarted = true;

      for (var i = 0; i < words.length; i++) {
        word = words[i];
        wordWidth = this._measureText(ctx, word, lineIndex, offset);

        offset += word.length;

        lineWidth += infixWidth + wordWidth;

        if (lineWidth >= this.width && !lineJustStarted) {
          lines.push(line);
          line = '';
          lineWidth = wordWidth;
          lineJustStarted = true;
        }

        if (!lineJustStarted) {
          line += infix;
        }
        line += word;

        infixWidth = this._measureText(ctx, infix, lineIndex, offset);
        offset++;
        lineJustStarted = false;
        // keep track of largest word
        if (wordWidth > largestWordWidth) {
          largestWordWidth = wordWidth;
        }
      }

      i && lines.push(line);

      if (largestWordWidth > this.dynamicMinWidth) {
        this.dynamicMinWidth = largestWordWidth;
      }

      return lines;
    },
    /**
     * Gets lines of text to render in the Textbox. This function calculates
     * text wrapping on the fly everytime it is called.
     * @returns {Array} Array of lines in the Textbox.
     * @override
     */
    _splitTextIntoLines: function() {
      var originalAlign = this.textAlign;
      this.ctx.save();
      this._setTextStyles(this.ctx);
      this.textAlign = 'left';
      var lines = this._wrapText(this.ctx, this.text);
      this.textAlign = originalAlign;
      this.ctx.restore();
      this._textLines = lines;
      this._styleMap = this._generateStyleMap();
      return lines;
    },

    /**
     * When part of a group, we don't want the Textbox's scale to increase if
     * the group's increases. That's why we reduce the scale of the Textbox by
     * the amount that the group's increases. This is to maintain the effective
     * scale of the Textbox at 1, so that font-size values make sense. Otherwise
     * the same font-size value would result in different actual size depending
     * on the value of the scale.
     * @param {String} key
     * @param {Any} value
     */
    setOnGroup: function(key, value) {
      if (key === 'scaleX') {
        this.set('scaleX', Math.abs(1 / value));
        this.set('width', (this.get('width') * value) /
          (typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX));
        this.__oldScaleX = value;
      }
    },

    /**
     * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start).
     * Overrides the superclass function to take into account text wrapping.
     *
     * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
     */
    get2DCursorLocation: function(selectionStart) {
      if (typeof selectionStart === 'undefined') {
        selectionStart = this.selectionStart;
      }

      var numLines = this._textLines.length,
          removed  = 0;

      for (var i = 0; i < numLines; i++) {
        var line    = this._textLines[i],
            lineLen = line.length;

        if (selectionStart <= removed + lineLen) {
          return {
            lineIndex: i,
            charIndex: selectionStart - removed
          };
        }

        removed += lineLen;

        if (this.text[removed] === '\n' || this.text[removed] === ' ') {
          removed++;
        }
      }

      return {
        lineIndex: numLines - 1,
        charIndex: this._textLines[numLines - 1].length
      };
    },

    /**
     * Overrides superclass function and uses text wrapping data to get cursor
     * boundary offsets instead of the array of chars.
     * @param {Array} chars Unused
     * @param {String} typeOfBoundaries Can be 'cursor' or 'selection'
     * @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set.
     */
    _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) {
      var topOffset      = 0,
          leftOffset     = 0,
          cursorLocation = this.get2DCursorLocation(),
          lineChars      = this._textLines[cursorLocation.lineIndex].split(''),
          lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex));

      for (var i = 0; i < cursorLocation.charIndex; i++) {
        leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i);
      }

      for (i = 0; i < cursorLocation.lineIndex; i++) {
        topOffset += this._getHeightOfLine(this.ctx, i);
      }

      if (typeOfBoundaries === 'cursor') {
        topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex)
          / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)
          * (1 - this._fontSizeFraction);
      }

      return {
        top: topOffset,
        left: leftOffset,
        lineLeft: lineLeftOffset
      };
    },

    getMinWidth: function() {
      return Math.max(this.minWidth, this.dynamicMinWidth);
    },

    /**
     * Returns object representation of an instance
     * @method toObject
     * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
     * @return {Object} object representation of an instance
     */
    toObject: function(propertiesToInclude) {
      return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
        minWidth: this.minWidth
      });
    }
  });
  /**
   * Returns fabric.Textbox instance from an object representation
   * @static
   * @memberOf fabric.Textbox
   * @param {Object} object Object to create an instance from
   * @return {fabric.Textbox} instance of fabric.Textbox
   */
  fabric.Textbox.fromObject = function(object) {
    return new fabric.Textbox(object.text, clone(object));
  };
  /**
   * Returns the default controls visibility required for Textboxes.
   * @returns {Object}
   */
  fabric.Textbox.getTextboxControlVisibility = function() {
    return {
      tl: false,
      tr: false,
      br: false,
      bl: false,
      ml: true,
      mt: false,
      mr: true,
      mb: false,
      mtr: true
    };
  };
  /**
   * Contains all fabric.Textbox objects that have been created
   * @static
   * @memberOf fabric.Textbox
   * @type Array
   */
  fabric.Textbox.instances = [];
})(typeof exports !== 'undefined' ? exports : this);
<commit_msg>remove Textbox instances (#3008)<commit_after>(function(global) {

  'use strict';

  var fabric = global.fabric || (global.fabric = {}),
      clone  = fabric.util.object.clone;

  /**
   * Textbox class, based on IText, allows the user to resize the text rectangle
   * and wraps lines automatically. Textboxes have their Y scaling locked, the
   * user can only change width. Height is adjusted automatically based on the
   * wrapping of lines.
   * @class fabric.Textbox
   * @extends fabric.IText
   * @mixes fabric.Observable
   * @return {fabric.Textbox} thisArg
   * @see {@link fabric.Textbox#initialize} for constructor definition
   */
  fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {

    /**
     * Type of an object
     * @type String
     * @default
     */
    type: 'textbox',

    /**
     * Minimum width of textbox, in pixels.
     * @type Number
     * @default
     */
    minWidth: 20,

    /**
     * Minimum calculated width of a textbox, in pixels.
     * @type Number
     * @default
     */
    dynamicMinWidth: 0,

    /**
     * Cached array of text wrapping.
     * @type Array
     */
    __cachedLines: null,

    /**
     * Override standard Object class values
     */
    lockScalingY: true,

    /**
     * Override standard Object class values
     */
    lockScalingFlip: true,

    /**
     * Constructor. Some scaling related property values are forced. Visibility
     * of controls is also fixed; only the rotation and width controls are
     * made available.
     * @param {String} text Text string
     * @param {Object} [options] Options object
     * @return {fabric.Textbox} thisArg
     */
    initialize: function(text, options) {
      this.ctx = fabric.util.createCanvasElement().getContext('2d');
      this.callSuper('initialize', text, options);
      this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility());

      // add width to this list of props that effect line wrapping.
      this._dimensionAffectingProps.width = true;
    },

    /**
     * Unlike superclass's version of this function, Textbox does not update
     * its width.
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @private
     * @override
     */
    _initDimensions: function(ctx) {
      if (this.__skipDimension) {
        return;
      }

      if (!ctx) {
        ctx = fabric.util.createCanvasElement().getContext('2d');
        this._setTextStyles(ctx);
      }

      // clear dynamicMinWidth as it will be different after we re-wrap line
      this.dynamicMinWidth = 0;

      // wrap lines
      this._textLines = this._splitTextIntoLines();

      // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
      if (this.dynamicMinWidth > this.width) {
        this._set('width', this.dynamicMinWidth);
      }

      // clear cache and re-calculate height
      this._clearCache();
      this.height = this._getTextHeight(ctx);
    },

    /**
     * Generate an object that translates the style object so that it is
     * broken up by visual lines (new lines and automatic wrapping).
     * The original text styles object is broken up by actual lines (new lines only),
     * which is only sufficient for Text / IText
     * @private
     */
    _generateStyleMap: function() {
      var realLineCount     = 0,
          realLineCharCount = 0,
          charCount         = 0,
          map               = {};

      for (var i = 0; i < this._textLines.length; i++) {
        if (this.text[charCount] === '\n') {
          realLineCharCount = 0;
          charCount++;
          realLineCount++;
        }
        else if (this.text[charCount] === ' ') {
          // this case deals with space's that are removed from end of lines when wrapping
          realLineCharCount++;
          charCount++;
        }

        map[i] = { line: realLineCount, offset: realLineCharCount };

        charCount += this._textLines[i].length;
        realLineCharCount += this._textLines[i].length;
      }

      return map;
    },

    /**
     * @param {Number} lineIndex
     * @param {Number} charIndex
     * @param {Boolean} [returnCloneOrEmpty=false]
     * @private
     */
    _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) {
      if (this._styleMap) {
        var map = this._styleMap[lineIndex];
        if (!map) {
          return returnCloneOrEmpty ? { } : null;
        }
        lineIndex = map.line;
        charIndex = map.offset + charIndex;
      }
      return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty);
    },

    /**
     * @param {Number} lineIndex
     * @param {Number} charIndex
     * @param {Object} style
     * @private
     */
    _setStyleDeclaration: function(lineIndex, charIndex, style) {
      var map = this._styleMap[lineIndex];
      lineIndex = map.line;
      charIndex = map.offset + charIndex;

      this.styles[lineIndex][charIndex] = style;
    },

    /**
     * @param {Number} lineIndex
     * @param {Number} charIndex
     * @private
     */
    _deleteStyleDeclaration: function(lineIndex, charIndex) {
      var map = this._styleMap[lineIndex];
      lineIndex = map.line;
      charIndex = map.offset + charIndex;

      delete this.styles[lineIndex][charIndex];
    },

    /**
     * @param {Number} lineIndex
     * @private
     */
    _getLineStyle: function(lineIndex) {
      var map = this._styleMap[lineIndex];
      return this.styles[map.line];
    },

    /**
     * @param {Number} lineIndex
     * @param {Object} style
     * @private
     */
    _setLineStyle: function(lineIndex, style) {
      var map = this._styleMap[lineIndex];
      this.styles[map.line] = style;
    },

    /**
     * @param {Number} lineIndex
     * @private
     */
    _deleteLineStyle: function(lineIndex) {
      var map = this._styleMap[lineIndex];
      delete this.styles[map.line];
    },

    /**
     * Wraps text using the 'width' property of Textbox. First this function
     * splits text on newlines, so we preserve newlines entered by the user.
     * Then it wraps each line using the width of the Textbox by calling
     * _wrapLine().
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @param {String} text The string of text that is split into lines
     * @returns {Array} Array of lines
     */
    _wrapText: function(ctx, text) {
      var lines = text.split(this._reNewline), wrapped = [], i;

      for (i = 0; i < lines.length; i++) {
        wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i));
      }

      return wrapped;
    },

    /**
     * Helper function to measure a string of text, given its lineIndex and charIndex offset
     *
     * @param {CanvasRenderingContext2D} ctx
     * @param {String} text
     * @param {number} lineIndex
     * @param {number} charOffset
     * @returns {number}
     * @private
     */
    _measureText: function(ctx, text, lineIndex, charOffset) {
      var width = 0;
      charOffset = charOffset || 0;

      for (var i = 0, len = text.length; i < len; i++) {
        width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset);
      }

      return width;
    },

    /**
     * Wraps a line of text using the width of the Textbox and a context.
     * @param {CanvasRenderingContext2D} ctx Context to use for measurements
     * @param {String} text The string of text to split into lines
     * @param {Number} lineIndex
     * @returns {Array} Array of line(s) into which the given text is wrapped
     * to.
     */
    _wrapLine: function(ctx, text, lineIndex) {
      var lineWidth        = 0,
          lines            = [],
          line             = '',
          words            = text.split(' '),
          word             = '',
          offset           = 0,
          infix            = ' ',
          wordWidth        = 0,
          infixWidth       = 0,
          largestWordWidth = 0,
          lineJustStarted = true;

      for (var i = 0; i < words.length; i++) {
        word = words[i];
        wordWidth = this._measureText(ctx, word, lineIndex, offset);

        offset += word.length;

        lineWidth += infixWidth + wordWidth;

        if (lineWidth >= this.width && !lineJustStarted) {
          lines.push(line);
          line = '';
          lineWidth = wordWidth;
          lineJustStarted = true;
        }

        if (!lineJustStarted) {
          line += infix;
        }
        line += word;

        infixWidth = this._measureText(ctx, infix, lineIndex, offset);
        offset++;
        lineJustStarted = false;
        // keep track of largest word
        if (wordWidth > largestWordWidth) {
          largestWordWidth = wordWidth;
        }
      }

      i && lines.push(line);

      if (largestWordWidth > this.dynamicMinWidth) {
        this.dynamicMinWidth = largestWordWidth;
      }

      return lines;
    },
    /**
     * Gets lines of text to render in the Textbox. This function calculates
     * text wrapping on the fly everytime it is called.
     * @returns {Array} Array of lines in the Textbox.
     * @override
     */
    _splitTextIntoLines: function() {
      var originalAlign = this.textAlign;
      this.ctx.save();
      this._setTextStyles(this.ctx);
      this.textAlign = 'left';
      var lines = this._wrapText(this.ctx, this.text);
      this.textAlign = originalAlign;
      this.ctx.restore();
      this._textLines = lines;
      this._styleMap = this._generateStyleMap();
      return lines;
    },

    /**
     * When part of a group, we don't want the Textbox's scale to increase if
     * the group's increases. That's why we reduce the scale of the Textbox by
     * the amount that the group's increases. This is to maintain the effective
     * scale of the Textbox at 1, so that font-size values make sense. Otherwise
     * the same font-size value would result in different actual size depending
     * on the value of the scale.
     * @param {String} key
     * @param {Any} value
     */
    setOnGroup: function(key, value) {
      if (key === 'scaleX') {
        this.set('scaleX', Math.abs(1 / value));
        this.set('width', (this.get('width') * value) /
          (typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX));
        this.__oldScaleX = value;
      }
    },

    /**
     * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start).
     * Overrides the superclass function to take into account text wrapping.
     *
     * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
     */
    get2DCursorLocation: function(selectionStart) {
      if (typeof selectionStart === 'undefined') {
        selectionStart = this.selectionStart;
      }

      var numLines = this._textLines.length,
          removed  = 0;

      for (var i = 0; i < numLines; i++) {
        var line    = this._textLines[i],
            lineLen = line.length;

        if (selectionStart <= removed + lineLen) {
          return {
            lineIndex: i,
            charIndex: selectionStart - removed
          };
        }

        removed += lineLen;

        if (this.text[removed] === '\n' || this.text[removed] === ' ') {
          removed++;
        }
      }

      return {
        lineIndex: numLines - 1,
        charIndex: this._textLines[numLines - 1].length
      };
    },

    /**
     * Overrides superclass function and uses text wrapping data to get cursor
     * boundary offsets instead of the array of chars.
     * @param {Array} chars Unused
     * @param {String} typeOfBoundaries Can be 'cursor' or 'selection'
     * @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set.
     */
    _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) {
      var topOffset      = 0,
          leftOffset     = 0,
          cursorLocation = this.get2DCursorLocation(),
          lineChars      = this._textLines[cursorLocation.lineIndex].split(''),
          lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex));

      for (var i = 0; i < cursorLocation.charIndex; i++) {
        leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i);
      }

      for (i = 0; i < cursorLocation.lineIndex; i++) {
        topOffset += this._getHeightOfLine(this.ctx, i);
      }

      if (typeOfBoundaries === 'cursor') {
        topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex)
          / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)
          * (1 - this._fontSizeFraction);
      }

      return {
        top: topOffset,
        left: leftOffset,
        lineLeft: lineLeftOffset
      };
    },

    getMinWidth: function() {
      return Math.max(this.minWidth, this.dynamicMinWidth);
    },

    /**
     * Returns object representation of an instance
     * @method toObject
     * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
     * @return {Object} object representation of an instance
     */
    toObject: function(propertiesToInclude) {
      return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
        minWidth: this.minWidth
      });
    }
  });
  /**
   * Returns fabric.Textbox instance from an object representation
   * @static
   * @memberOf fabric.Textbox
   * @param {Object} object Object to create an instance from
   * @return {fabric.Textbox} instance of fabric.Textbox
   */
  fabric.Textbox.fromObject = function(object) {
    return new fabric.Textbox(object.text, clone(object));
  };
  /**
   * Returns the default controls visibility required for Textboxes.
   * @returns {Object}
   */
  fabric.Textbox.getTextboxControlVisibility = function() {
    return {
      tl: false,
      tr: false,
      br: false,
      bl: false,
      ml: true,
      mt: false,
      mr: true,
      mb: false,
      mtr: true
    };
  };

})(typeof exports !== 'undefined' ? exports : this);
<|endoftext|>


<commit_before>var request = require('request');

var MAX_DAYS = 100;
var MS_IN_DAY = 24 * 60 * 60 * 1000;
var apiUrl;
var acts = [
  'Unspecified',
  'Sleep',
  'Eating',
  'Other personal care',
  'Main and second job',
  'Employment activities',
  'School and university',
  'Homework',
  'Freetime study',
  'Food preparation',
  'Dish washing',
  'Cleaning dwelling',
  'Other household upkeep',
  'Laundry',
  'Ironing',
  'Handicraft',
  'Gardening',
  'Tending domestic animals',
  'Caring for pets',
  'Walking the dog',
  'Construction and repairs',
  'Shopping and services',
  'Child care',
  'Playing with and teaching kids',
  'Other domestic work',
  'Organisational work',
  'Help to other households',
  'Participatory activities',
  'Visits and feasts',
  'Other social life',
  'Entertainment and culture',
  'Resting',
  'Walking and hiking',
  'Sports and outdoors',
  'Computer and video games',
  'Other computing',
  'Other hobbies and games',
  'Reading books',
  'Other reading',
  'TV and video',
  'Radio and music',
  'Unspecified leisure',
  'Travel to/from work',
  'Travel related to study',
  'Travel related to shopping',
  'Transporting a child',
  'Travel related to other domestic',
  'Travel related to leisure',
  'Unspecified travel',
  'Unspecified'
];

var locs = {
  '10': 'Unspecified',
  '11': 'Home',
  '12': 'Second home',
  '13': 'Workplace/school',
  '14': 'Other\'s home',
  '15': 'Restaurant',
  '16': 'Shop, market',
  '17': 'Hotel, camping',
  '19': 'Other',
  '20': 'Unspecified',
  '21': 'Walking, waiting',
  '22': 'Bicycle',
  '23': 'Motorbike',
  '24': 'Car',
  '29': 'Other private',
  '31': 'Public transport'
};

var parents = [];
for (var i=1; i<acts.length; i++) {
  if (i <= 3) {
    parents[i] = 'Personal care';
  }
  else if (i <= 5) {
    parents[i] = 'Employment';
  }
  else if (i <= 8) {
    parents[i] = 'Study';
  }
  else if (i <= 24) {
    parents[i] = 'Domestic';
  }
  else if (i <= 41) {
    parents[i] = 'Leisure';
  }
  else if (i <= 48) {
    parents[i] = 'Travel';
  }
  else {
    parents[i] = 'Unspecified';
  }
}

var withValues = ['', 'alone', 'partner', 'parent', 'kids', 'family', 'others'];

var adapter = {};

adapter.sensorName = 'tracktime';

adapter.types = [
  {
    name: 'tracktime',
    fields: {
      id: 'keyword',
      timestamp: 'date',
      starttime: 'date',
      starthour: 'integer',
      endtime: 'date',
      sliceid: 'integer',
      slice: 'keyword',
      duration: 'integer',
      mainid: 'integer',
      mainaction: 'keyword',
      maincategory: 'keyword',
      sideid: 'integer',
      sideaction: 'keyword',
      sidecategory: 'keyword',
      withid: 'integer',
      with: 'keyword',
      usecomputer: 'boolean',
      location: 'keyword',
      locid: 'integer',
      description: 'text',
      rating: 'integer'
    }
  }
];

adapter.promptProps = {
  properties: {
    url: {
      description: 'Enter your TrackTime URL'.magenta
    }
  }
};

adapter.storeConfig = function(c8, result) {
  return c8.config(result).then(function(){
    console.log('Configuration stored.');
    c8.release();
  });
}

adapter.importData = function(c8, conf, opts) {
  return new Promise(function (fulfill, reject){
    c8.search({
      _source: ['timestamp'],
      size: 1,
      sort: [{'timestamp': 'desc'}],
    }).then(function(response) {
      var resp = c8.trimResults(response);
      if (opts.firstDate) {
        firstDate = new Date(opts.firstDate);
        console.log('Setting first time to ' + firstDate);
      }
      else if (resp && resp.timestamp) {
        var d = new Date(resp.timestamp);
        firstDate = new Date(d.getTime() + 1);
        console.log('Setting first time to ' + firstDate);
      }
      else {
        firstDate = new Date();
        firstDate.setMonth(firstDate.getMonth() - 1);
        console.warn('No previously indexed data, setting first time to ' + firstDate);
      }
      if (opts.lastDate) {
        lastDate = new Date(opts.lastDate);
      }
      else {
        lastDate = new Date();
      }
      if (lastDate.getTime() >= (firstDate.getTime() + (MAX_DAYS * MS_IN_DAY))) {
        lastDate.setTime(firstDate.getTime() + (MAX_DAYS * MS_IN_DAY));
        console.warn('Max date range %d days, setting lastDate to %s', MAX_DAYS, lastDate);
      }
      var url = conf.url + '?starttime=' + Math.floor(firstDate/1000);
      if (lastDate) {
        console.log("Setting last time to " + lastDate);
        url += '&endtime=' + Math.ceil(lastDate/1000);
      }
      var cookieJar = request.jar();
      // console.log(url);
      request({url: url, jar: cookieJar}, function(error, response, body) {
        if (error || !response || !body) {
          // console.warn('Error getting data: ' + JSON.stringify(response.body));
        }
        // console.log(body);
        var data = JSON.parse(body);
        if (data && data.length) {
          var bulk = [];
          for (var i=data.length-1; i>=0; i--) {
            // bulk.push({index: {_index: c8._index, _type: c8._type, _id: data[i].id}});
            data[i].starttime = new Date(data[i].starttime);
            data[i].endtime = new Date(data[i].endtime);
            data[i].duration = (data[i].endtime - data[i].starttime)/1000;
            data[i].timestamp = data[i].starttime;
            data[i].mainid = data[i].mainaction;
            data[i].mainaction = acts[data[i].mainaction];
            data[i].maincategory = parents[data[i].mainid];
            data[i].sideid = data[i].sideaction;
            data[i].sideaction = acts[data[i].sideaction];
            data[i].sidecategory = parents[data[i].sideid];
            data[i].locid = data[i].location;
            data[i].location = locs[data[i].location];
            data[i].withid = data[i]['with'];
            if (data[i] == 1) {
              data[i].withid = ['alone'];
            }
            else {
              data[i]['with'] = [];
              for (var j=0; j<=6; j++) {
                if (data[i].withid & Math.pow(2, (j-1))) {
                  data[i]['with'].push(withValues[j].toLowerCase());
                }
              }
            }
            // split into 10 minute slices
            var tenMinutes = 10 * 60 * 1000;
            var start = data[i].starttime.getTime();
            var end = data[i].endtime.getTime();
            for (var t = start; t < end; t += tenMinutes) {
              var copy = JSON.parse(JSON.stringify(data[i]));
              // var id = t + '-' + data[i].id;
              var sliceTime = new Date(t);
              var startHour = sliceTime.getHours();
              var startMinute = sliceTime.getMinutes();
              copy.slice = [startHour, startMinute].join(':');
              if (startMinute == 0) {
                copy.slice += '0';
              }
              var idTime = startHour + startMinute/60;
              copy.sliceid = Math.round((idTime + (idTime >= 4 ? -4 : 20)) * 6);
              copy.timestamp = t;
              copy.starttime = t;
              copy.starthour = startHour;
              copy.endtime = t + tenMinutes;
              copy.duration = tenMinutes / 1000;
              bulk.push({index: {_index: c8._index, _type: c8._type, _id: t}});
              bulk.push(copy);
              // console.log(JSON.stringify(copy.sliceId) + ': ' + sliceTime);
            }
            console.log(data[i].timestamp);
          }
          if (bulk.length > 0) {
            c8.bulk(bulk).then(function(result) {
              if (result.errors) {
                var messages = [];
                for (var i=0; i<result.items.length; i++) {
                  if (result.items[i].index.error) {
                    messages.push(i + ': ' + result.items[i].index.error.reason);
                  }
                }
                reject(new Error(messages.length + ' errors in bulk insert:\n ' + messages.join('\n ')));
              }
              fulfill('Indexed ' + result.items.length + ' documents in ' + result.took + ' ms.');
            }).catch(function(error) {
              reject(error);
            });
          }
          else {
            fulfill('No data to import.');
          }
        }
      });
    }).catch(function(error) {
      reject(error);
      c8.release();
    });
  });
};

module.exports = adapter;
<commit_msg>explicitly cast usecomputer to boolean<commit_after>var request = require('request');

var MAX_DAYS = 100;
var MS_IN_DAY = 24 * 60 * 60 * 1000;
var apiUrl;
var acts = [
  'Unspecified',
  'Sleep',
  'Eating',
  'Other personal care',
  'Main and second job',
  'Employment activities',
  'School and university',
  'Homework',
  'Freetime study',
  'Food preparation',
  'Dish washing',
  'Cleaning dwelling',
  'Other household upkeep',
  'Laundry',
  'Ironing',
  'Handicraft',
  'Gardening',
  'Tending domestic animals',
  'Caring for pets',
  'Walking the dog',
  'Construction and repairs',
  'Shopping and services',
  'Child care',
  'Playing with and teaching kids',
  'Other domestic work',
  'Organisational work',
  'Help to other households',
  'Participatory activities',
  'Visits and feasts',
  'Other social life',
  'Entertainment and culture',
  'Resting',
  'Walking and hiking',
  'Sports and outdoors',
  'Computer and video games',
  'Other computing',
  'Other hobbies and games',
  'Reading books',
  'Other reading',
  'TV and video',
  'Radio and music',
  'Unspecified leisure',
  'Travel to/from work',
  'Travel related to study',
  'Travel related to shopping',
  'Transporting a child',
  'Travel related to other domestic',
  'Travel related to leisure',
  'Unspecified travel',
  'Unspecified'
];

var locs = {
  '10': 'Unspecified',
  '11': 'Home',
  '12': 'Second home',
  '13': 'Workplace/school',
  '14': 'Other\'s home',
  '15': 'Restaurant',
  '16': 'Shop, market',
  '17': 'Hotel, camping',
  '19': 'Other',
  '20': 'Unspecified',
  '21': 'Walking, waiting',
  '22': 'Bicycle',
  '23': 'Motorbike',
  '24': 'Car',
  '29': 'Other private',
  '31': 'Public transport'
};

var parents = [];
for (var i=1; i<acts.length; i++) {
  if (i <= 3) {
    parents[i] = 'Personal care';
  }
  else if (i <= 5) {
    parents[i] = 'Employment';
  }
  else if (i <= 8) {
    parents[i] = 'Study';
  }
  else if (i <= 24) {
    parents[i] = 'Domestic';
  }
  else if (i <= 41) {
    parents[i] = 'Leisure';
  }
  else if (i <= 48) {
    parents[i] = 'Travel';
  }
  else {
    parents[i] = 'Unspecified';
  }
}

var withValues = ['', 'alone', 'partner', 'parent', 'kids', 'family', 'others'];

var adapter = {};

adapter.sensorName = 'tracktime';

adapter.types = [
  {
    name: 'tracktime',
    fields: {
      id: 'keyword',
      timestamp: 'date',
      starttime: 'date',
      starthour: 'integer',
      endtime: 'date',
      sliceid: 'integer',
      slice: 'keyword',
      duration: 'integer',
      mainid: 'integer',
      mainaction: 'keyword',
      maincategory: 'keyword',
      sideid: 'integer',
      sideaction: 'keyword',
      sidecategory: 'keyword',
      withid: 'integer',
      with: 'keyword',
      usecomputer: 'boolean',
      location: 'keyword',
      locid: 'integer',
      description: 'text',
      rating: 'integer'
    }
  }
];

adapter.promptProps = {
  properties: {
    url: {
      description: 'Enter your TrackTime URL'.magenta
    }
  }
};

adapter.storeConfig = function(c8, result) {
  return c8.config(result).then(function(){
    console.log('Configuration stored.');
    c8.release();
  });
}

adapter.importData = function(c8, conf, opts) {
  return new Promise(function (fulfill, reject){
    c8.search({
      _source: ['timestamp'],
      size: 1,
      sort: [{'timestamp': 'desc'}],
    }).then(function(response) {
      var resp = c8.trimResults(response);
      if (opts.firstDate) {
        firstDate = new Date(opts.firstDate);
        console.log('Setting first time to ' + firstDate);
      }
      else if (resp && resp.timestamp) {
        var d = new Date(resp.timestamp);
        firstDate = new Date(d.getTime() + 1);
        console.log('Setting first time to ' + firstDate);
      }
      else {
        firstDate = new Date();
        firstDate.setMonth(firstDate.getMonth() - 1);
        console.warn('No previously indexed data, setting first time to ' + firstDate);
      }
      if (opts.lastDate) {
        lastDate = new Date(opts.lastDate);
      }
      else {
        lastDate = new Date();
      }
      if (lastDate.getTime() >= (firstDate.getTime() + (MAX_DAYS * MS_IN_DAY))) {
        lastDate.setTime(firstDate.getTime() + (MAX_DAYS * MS_IN_DAY));
        console.warn('Max date range %d days, setting lastDate to %s', MAX_DAYS, lastDate);
      }
      var url = conf.url + '?starttime=' + Math.floor(firstDate/1000);
      if (lastDate) {
        console.log("Setting last time to " + lastDate);
        url += '&endtime=' + Math.ceil(lastDate/1000);
      }
      var cookieJar = request.jar();
      // console.log(url);
      request({url: url, jar: cookieJar}, function(error, response, body) {
        if (error || !response || !body) {
          // console.warn('Error getting data: ' + JSON.stringify(response.body));
        }
        // console.log(body);
        var data = JSON.parse(body);
        if (data && data.length) {
          var bulk = [];
          for (var i=data.length-1; i>=0; i--) {
            // bulk.push({index: {_index: c8._index, _type: c8._type, _id: data[i].id}});
            data[i].starttime = new Date(data[i].starttime);
            data[i].endtime = new Date(data[i].endtime);
            data[i].duration = (data[i].endtime - data[i].starttime)/1000;
            data[i].timestamp = data[i].starttime;
            data[i].mainid = data[i].mainaction;
            data[i].mainaction = acts[data[i].mainaction];
            data[i].maincategory = parents[data[i].mainid];
            data[i].sideid = data[i].sideaction;
            data[i].sideaction = acts[data[i].sideaction];
            data[i].sidecategory = parents[data[i].sideid];
            data[i].usecomputer = data[i].usecomputer ? true : false;
            data[i].locid = data[i].location;
            data[i].location = locs[data[i].location];
            data[i].withid = data[i]['with'];
            if (data[i] == 1) {
              data[i].withid = ['alone'];
            }
            else {
              data[i]['with'] = [];
              for (var j=0; j<=6; j++) {
                if (data[i].withid & Math.pow(2, (j-1))) {
                  data[i]['with'].push(withValues[j].toLowerCase());
                }
              }
            }
            // split into 10 minute slices
            var tenMinutes = 10 * 60 * 1000;
            var start = data[i].starttime.getTime();
            var end = data[i].endtime.getTime();
            for (var t = start; t < end; t += tenMinutes) {
              var copy = JSON.parse(JSON.stringify(data[i]));
              // var id = t + '-' + data[i].id;
              var sliceTime = new Date(t);
              var startHour = sliceTime.getHours();
              var startMinute = sliceTime.getMinutes();
              copy.slice = [startHour, startMinute].join(':');
              if (startMinute == 0) {
                copy.slice += '0';
              }
              var idTime = startHour + startMinute/60;
              copy.sliceid = Math.round((idTime + (idTime >= 4 ? -4 : 20)) * 6);
              copy.timestamp = t;
              copy.starttime = t;
              copy.starthour = startHour;
              copy.endtime = t + tenMinutes;
              copy.duration = tenMinutes / 1000;
              bulk.push({index: {_index: c8._index, _type: c8._type, _id: t}});
              bulk.push(copy);
              // console.log(JSON.stringify(copy.sliceId) + ': ' + sliceTime);
            }
            console.log(data[i].timestamp);
          }
          if (bulk.length > 0) {
            c8.bulk(bulk).then(function(result) {
              if (result.errors) {
                var messages = [];
                for (var i=0; i<result.items.length; i++) {
                  if (result.items[i].index.error) {
                    messages.push(i + ': ' + result.items[i].index.error.reason);
                  }
                }
                reject(new Error(messages.length + ' errors in bulk insert:\n ' + messages.join('\n ')));
              }
              fulfill('Indexed ' + result.items.length + ' documents in ' + result.took + ' ms.');
            }).catch(function(error) {
              reject(error);
            });
          }
          else {
            fulfill('No data to import.');
          }
        }
      });
    }).catch(function(error) {
      reject(error);
      c8.release();
    });
  });
};

module.exports = adapter;
<|endoftext|>


<commit_before>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var fs = require('fs');
var path = require('path');

var hooks = require('deployment/hooks');
var deployTemplates = require('deployment/templates');
var deployServices = require('deployment/services');
var deployFiles = require('deployment/files');

var serviceManagement = require('service_management');
var manifest = require('manifest');
var manifestConstants = require('manifest/constants');
var config = require('util/config');
var fsutil = require('util/fs');
var flowctrl = require('util/flow_control');

var async = require('async');
var sprintf = require('sprintf').sprintf;

var INSTANCE_NAME_RE = /^[a-zA-Z0-9_\-]+$/;

function Instance(name) {
  this.name = name;
  this.root = path.join(config.get()['app_dir'], name);
  this._bundleName = null;
}

/**
 * Check if an instance exists
 *
 * @param {String} instanceName The name of the instance.
 * @param {Function} callback Callback which is called with a boolean 'exists'.
 */
Instance.prototype.exists = function(callback) {
  fs.stat(this.root, function(err, stats) {
    if (!err && stats.isDirectory()) {
      callback(true);
      return;
    }

    callback(false);
    return;
  });
};

/**
 * Get the name of the bundle used by an instance. This is probably the
 * nastiest part of this whole instance filesystem layout because it requires
 * reading a link. To keep things fast this gets cached on the Instance object.
 *
 * @param {Function} callback Callback which is called with (err, bundleName).
 */
Instance.prototype.getBundleName = function(callback) {
  var self = this;
  if (self._bundleName) {
    process.nextTick(function() {
      callback(null, self._bundleName);
    });
  }
  else {
    var bundleLinkPath = path.join(self.root, 'bundle');
    fs.readlink(bundleLinkPath, function(err, target) {
      if (!err) {
        self._bundleName = path.basename(target);
      }
      callback(err, self._bundleName);
      return;
    });
  }
};

/**
 * Get the version pointed to by the 'current' link. This will give a value of
 * 'null' if the current link doesn't exist (currently it should always exist
 * but we may want to introduce situations where it won't in the future).
 *
 * @param {Function} callback Callback which is called with (version).
 */
Instance.prototype.getBundleVersion = function(callback) {
  var self = this;
  var currentLinkPath = path.join(self.root, 'current');
  fs.readlink(currentLinkPath, function(err, target) {
    if (err) {
      callback(null);
      return;
    }
    else {
      callback(target.split('@', 2)[1]);
      return;
    }
  });
};

/**
 * Get the path at which a version of this instance would reside. This does
 * not guarantee that this version exists, merely returns the hypothetical
 * path at which it would reside.
 *
 * @param {String} version    The version to get the path to.
 * @param {Function} callback A callback that takes (err, versionPath).
 */
Instance.prototype.getVersionPath = function(version, callback) {
  var self = this;
  self.getBundleName(function(err, bundleName) {
    var bundleNameFull = bundleName + '@' + version;
    var versionPath = path.join(self.root, 'versions', bundleNameFull);
    callback(err, versionPath);
    return;
  });
};

/**
 * Get the path at which the extracted bundle for the specified version would
 * reside. Again, this doesn't verify that the path actually exists.
 *
 * @param {String} version    The version to get the path to.
 * @param {Function} callback A callback that takes (err, bundleVersionPath).
 */
Instance.prototype.getBundleVersionPath = function(version, callback) {
  var self = this;
  self.getBundleName(function(err, bundleName) {
    var bundleNameFull = bundleName + '@' + version;
    var bundleVersionPath = path.join(config.get()['extracted_dir'], bundleName, bundleNameFull);
    callback(err, bundleVersionPath);
    return;
  });
};

/**
 * Remove all data related to an instance, including the instance, its data,
 * and any services. This performs very little validation (doesn't even check
 * that the instance exists first), it just starts deleting stuff.
 *
 * @param {Function} callback Callback which is called when the cleanup process has finished.
 */
Instance.prototype.destroy = function(callback) {
  var manager = serviceManagement.getDefaultManager().getManager();

  var ops = [
    async.apply(flowctrl.callIgnoringError, fsutil.rmtree, null, this.root)
  ];

  manager.getService(this.name, function(err, service) {
    if (!err) {
      ops.push(async.apply(flowctrl.callIgnoringError, service.destroy, service));
    }

    async.parallel(ops, function(err) {
      callback();
    });
  });
};

/**
 * Given a bundle version, hard hard link all the files into place, render
 * any templates and resolve all data files.
 *
 * @param {String} version    The version of the bundle to prepare.
 * @param {Function} callback A callback fired with (err).
 */
Instance.prototype.prepareVersion = function(version, callback) {
  var self = this;
  var extractedBundlePath, bundleName, bundleNameFull, manifestObj;
  var instanceVersionPath, ignoredPaths;

  var bundleLinkPath = path.join(self.root, 'bundle');
  var hookEnv = { 'CAST_INSTANCE_NAME': self.name };

  async.series([
    // Resolve the path at which the version will reside
    function(callback) {
      self.getVersionPath(version, function(err, vp) {
        instanceVersionPath = vp;
        callback(err);
        return;
      });
    },

    // Resolve the path to the extracted bundle
    function(callback) {
      self.getBundleVersionPath(version, function(err, bvp) {
        extractedBundlePath = bvp;
        callback(err);
        return;
      });
    },

    // Make sure this version doesn't exist
    function(callback) {
      path.exists(instanceVersionPath, function(exists) {
        var err = null;
        if (exists) {
          err = new Error(sprintf('Instance \'%s\' already has version \'%s\'', self.name, version));
        }
        callback(err);
        return;
      });
    },

    // Make sure an extracted bundle exists for the requested version
    function(callback) {
      fs.stat(extractedBundlePath, function(err, stats) {
        if (!err && !stats.isDirectory()) {
          err = new Error('No bundle for version \'' + version + '\'');
        }
        callback(err);
        return;
      });
    },

    // Create the directory for the target version
    function(callback) {
      fsutil.ensureDirectory(instanceVersionPath, callback);
    },

    // Retrieve the manifest object
    function(callback) {
      var manifestPath = path.join(extractedBundlePath, manifestConstants.MANIFEST_FILENAME);
      manifest.getManifestObject(manifestPath, true, function(err, _manifestObj) {
        manifestObj = _manifestObj;
        ignoredPaths = manifestObj['template_files'].concat(manifestObj['data_files']);
        callback(err);
        return;
      });
    },

    // Mirror the directory structure from the extracted bundle in the instance directory
    function(callback) {
      fsutil.treeToTemplate(extractedBundlePath, ignoredPaths, function(err, templateObject) {
        if (err) {
          callback(err);
          return;
        }

        // No directories
        if (Object.keys(templateObject).length === 0) {
          callback();
          return;
        }

        fsutil.templateToTree(instanceVersionPath, templateObject, true, callback);
      });
    },

    // Realize (render and save) templates
    function(callback) {
      var instanceData = deployTemplates.getInstanceTemplateObject(self.name, instanceVersionPath);
      deployTemplates.realizeApplicationTemplates(manifestObj, instanceData, extractedBundlePath,
                                                     instanceVersionPath, callback);
    },

    // Hard-link all the files (except template and data files)
    function(callback) {
      fsutil.hardLinkFiles(extractedBundlePath, instanceVersionPath, ignoredPaths, callback);
    },

    // Resolve the data files
    function(callback) {
      var instanceDataRoot = path.join(self.root, 'data');
      deployFiles.resolveDataFiles(extractedBundlePath, instanceDataRoot, instanceVersionPath,
                                      manifestObj['data_files'], callback);
    },

    // Execute the 'post_prepare' hook
    function(callback) {
      var hook = new hooks.InstanceHook('post', 'post_prepare',
                                        instanceVersionPath, false, hookEnv);
      hook.execute(null, [version], callback);
    }
  ], callback);
};

/**
 * Point the 'current' symlink to the specified version. Will verify
 * that the specified version exists before taking action.
 *
 * @param {String} version    The version to activate.
 * @param {Function} callback A callback fired with (err).
 */
Instance.prototype.activateVersion = function(version, callback) {
  var self = this;
  var newVersionPath;

  var newVersionLink = path.join(self.root, 'new');
  var currentVersionLink = path.join(self.root, 'current');
  var hookEnv = { 'CAST_INSTANCE_NAME': self.name };

  async.series([
    // Get the path to the specified version
    function(callback) {
      self.getVersionPath(version, function(err, vp) {
        newVersionPath = vp;
        callback(err);
        return;
      });
    },

    // Make sure the version exists
    function(callback) {
      path.exists(newVersionPath, function(exists) {
        var err = null;
        if (!exists) {
          err = new Error('Cannot activate nonexistent version \'' + version + '\'');
        }

        callback(err);
        return;
      });
    },

    function(callback) {
      var hook = new hooks.InstanceHook('post', 'pre_version_activate',
                                        newVersionPath, false, hookEnv);
      hook.execute(null, [version, newVersionPath], callback);
    },

    // Create the new link
    function(callback) {
      fs.symlink(path.resolve(newVersionPath), newVersionLink, callback);
    },

    // Atomically move it into place
    async.apply(fs.rename, newVersionLink, currentVersionLink),

    function(callback) {
      var hook = new hooks.InstanceHook('post', 'post_version_activate',
                                        newVersionPath, false, hookEnv);
      hook.execute(null, [version, newVersionPath], callback);
    }
  ], callback);
};

/**
 * Create an instance with a specified name for the specified bundle. The
 * instance will initially use the specified version of the bundle.
 *
 * @param {String} instanceName  A name to give to the instance.
 * @param {String} bundleName    The name of the bundle to use.
 * @param {String} bundleVersion The version of the bundle to use.
 * @param {Function} callback     A callback fired with (err).
 */
exports.createInstance = function(instanceName, bundleName, bundleVersion, callback) {
  if (!INSTANCE_NAME_RE.exec(instanceName)) {
    callback(new Error('Invalid instance name'));
    return;
  }

  var manifestObj, ignoredPaths, instance;
  var bundleNameFull = bundleName + '@' + bundleVersion;

  // Get the paths to the extracted bundle and the manifest
  var extractedBundleRoot = path.join(config.get()['extracted_dir'], bundleName);
  var extractedBundlePath = path.join(extractedBundleRoot, bundleNameFull);

  var instancePath = path.join(config.get()['app_dir'], instanceName);
  var instanceDataRoot = path.join(instancePath, 'data');
  var instanceVersionsRoot = path.join(instancePath, 'versions');
  var instanceBundleLink = path.join(instancePath, 'bundle');
  var instanceCurrentLink = path.join(instancePath, 'current');
  var instanceVersionPath = path.join(instanceVersionsRoot, bundleNameFull);

  async.series([
    // Make sure that the extracted bundle path exists
    function(callback) {
      path.exists(extractedBundlePath, function(exists) {
        if (!exists) {
          callback(new Error('Invalid bundle name or version'));
          return;
        }

        callback();
      });
    },

    // Make sure there isn't already an instance with this name
    function(callback) {
      path.exists(instancePath, function(exists) {
        if (exists) {
          callback(new Error('Instance name already in use'));
          return;
        }
        callback();
      });
    },

    // Create the instance directory
    function(callback) {
      fsutil.ensureDirectory(instancePath, function(err) {
        if (!err) {
          instance = new Instance(instanceName);
        }
        callback(err);
      });
    },

    // Create the data and versions directories
    async.apply(fsutil.ensureDirectory, instanceDataRoot),
    async.apply(fsutil.ensureDirectory, instanceVersionsRoot),

    // Create the bundle symlink
    async.apply(fs.symlink, path.resolve(extractedBundleRoot), instanceBundleLink),

    // Prepare the specified version
    function(callback) {
      instance.prepareVersion(bundleVersion, callback);
    },

    // Symlink it
    function(callback) {
      instance.activateVersion(bundleVersion, callback);
    },

    // Retrieve the manifest object
    function(callback) {
      var manifestPath = path.join(extractedBundlePath, manifestConstants.MANIFEST_FILENAME);
      manifest.getManifestObject(manifestPath, true, function(err, _manifestObj) {
        manifestObj = _manifestObj;
        callback(err);
        return;
      });
    },

    // Create a runit service
    function(callback) {
      deployServices.createService(instanceName, instanceVersionPath, manifestObj, callback);
    }
  ],

  function(error) {
    if (error && instance) {
      instance.destroy(function() {
        callback(error);
        return;
      });
    }
    else {
      callback(error);
    }
  });
};

/**
 * Get an Instance object for an instance whose existence is verified.
 *
 * @param {String} instanceName  The name of the instance to retrieve.
 * @param {Function} callback     A callback that takes (err, instance).
 */
exports.getInstance = function(instanceName, callback) {
  var instance = new Instance(instanceName);
  instance.exists(function(exists) {
    if (!exists) {
      callback(new Error('Instance doesn\'t exist'));
      return;
    }
    else {
      callback(null, instance);
      return;
    }
  });
};

/**
 * Get a list of all instances (in the form of Instance objects).
 *
 * @param {Function} callback A callback that takes (err, instances).
 */
exports.getInstanceList = function(callback) {
  fs.readdir(config.get()['app_dir'], function(err, files) {
    if (err) {
      callback(err);
      return;
    }

    // Construct an Instance for each file in the app_dir
    var instanceList = files.map(function(file) {
      return new Instance(file);
    });

    // Make sure each instance actually exists (ie, isn't a non-directory of some sort)
    async.filter(instanceList, function(instance, callback) {
      instance.exists(callback);
    },
    function(instances) {
      callback(null, instances);
      return;
    });
  });
};

exports.Instance = Instance;
<commit_msg>Symlink bundle directory to "current" at the end of the instance create process.<commit_after>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var fs = require('fs');
var path = require('path');

var hooks = require('deployment/hooks');
var deployTemplates = require('deployment/templates');
var deployServices = require('deployment/services');
var deployFiles = require('deployment/files');

var serviceManagement = require('service_management');
var manifest = require('manifest');
var manifestConstants = require('manifest/constants');
var config = require('util/config');
var fsutil = require('util/fs');
var flowctrl = require('util/flow_control');

var async = require('async');
var sprintf = require('sprintf').sprintf;

var INSTANCE_NAME_RE = /^[a-zA-Z0-9_\-]+$/;

function Instance(name) {
  this.name = name;
  this.root = path.join(config.get()['app_dir'], name);
  this._bundleName = null;
}

/**
 * Check if an instance exists
 *
 * @param {String} instanceName The name of the instance.
 * @param {Function} callback Callback which is called with a boolean 'exists'.
 */
Instance.prototype.exists = function(callback) {
  fs.stat(this.root, function(err, stats) {
    if (!err && stats.isDirectory()) {
      callback(true);
      return;
    }

    callback(false);
    return;
  });
};

/**
 * Get the name of the bundle used by an instance. This is probably the
 * nastiest part of this whole instance filesystem layout because it requires
 * reading a link. To keep things fast this gets cached on the Instance object.
 *
 * @param {Function} callback Callback which is called with (err, bundleName).
 */
Instance.prototype.getBundleName = function(callback) {
  var self = this;
  if (self._bundleName) {
    process.nextTick(function() {
      callback(null, self._bundleName);
    });
  }
  else {
    var bundleLinkPath = path.join(self.root, 'bundle');
    fs.readlink(bundleLinkPath, function(err, target) {
      if (!err) {
        self._bundleName = path.basename(target);
      }
      callback(err, self._bundleName);
      return;
    });
  }
};

/**
 * Get the version pointed to by the 'current' link. This will give a value of
 * 'null' if the current link doesn't exist (currently it should always exist
 * but we may want to introduce situations where it won't in the future).
 *
 * @param {Function} callback Callback which is called with (version).
 */
Instance.prototype.getBundleVersion = function(callback) {
  var self = this;
  var currentLinkPath = path.join(self.root, 'current');
  fs.readlink(currentLinkPath, function(err, target) {
    if (err) {
      callback(null);
      return;
    }
    else {
      callback(target.split('@', 2)[1]);
      return;
    }
  });
};

/**
 * Get the path at which a version of this instance would reside. This does
 * not guarantee that this version exists, merely returns the hypothetical
 * path at which it would reside.
 *
 * @param {String} version    The version to get the path to.
 * @param {Function} callback A callback that takes (err, versionPath).
 */
Instance.prototype.getVersionPath = function(version, callback) {
  var self = this;
  self.getBundleName(function(err, bundleName) {
    var bundleNameFull = bundleName + '@' + version;
    var versionPath = path.join(self.root, 'versions', bundleNameFull);
    callback(err, versionPath);
    return;
  });
};

/**
 * Get the path at which the extracted bundle for the specified version would
 * reside. Again, this doesn't verify that the path actually exists.
 *
 * @param {String} version    The version to get the path to.
 * @param {Function} callback A callback that takes (err, bundleVersionPath).
 */
Instance.prototype.getBundleVersionPath = function(version, callback) {
  var self = this;
  self.getBundleName(function(err, bundleName) {
    var bundleNameFull = bundleName + '@' + version;
    var bundleVersionPath = path.join(config.get()['extracted_dir'], bundleName, bundleNameFull);
    callback(err, bundleVersionPath);
    return;
  });
};

/**
 * Remove all data related to an instance, including the instance, its data,
 * and any services. This performs very little validation (doesn't even check
 * that the instance exists first), it just starts deleting stuff.
 *
 * @param {Function} callback Callback which is called when the cleanup process has finished.
 */
Instance.prototype.destroy = function(callback) {
  var manager = serviceManagement.getDefaultManager().getManager();

  var ops = [
    async.apply(flowctrl.callIgnoringError, fsutil.rmtree, null, this.root)
  ];

  manager.getService(this.name, function(err, service) {
    if (!err) {
      ops.push(async.apply(flowctrl.callIgnoringError, service.destroy, service));
    }

    async.parallel(ops, function(err) {
      callback();
    });
  });
};

/**
 * Given a bundle version, hard hard link all the files into place, render
 * any templates and resolve all data files.
 *
 * @param {String} version    The version of the bundle to prepare.
 * @param {Function} callback A callback fired with (err).
 */
Instance.prototype.prepareVersion = function(version, callback) {
  var self = this;
  var extractedBundlePath, bundleName, bundleNameFull, manifestObj;
  var instanceVersionPath, ignoredPaths;

  var bundleLinkPath = path.join(self.root, 'bundle');
  var hookEnv = { 'CAST_INSTANCE_NAME': self.name };

  async.series([
    // Resolve the path at which the version will reside
    function(callback) {
      self.getVersionPath(version, function(err, vp) {
        instanceVersionPath = vp;
        callback(err);
        return;
      });
    },

    // Resolve the path to the extracted bundle
    function(callback) {
      self.getBundleVersionPath(version, function(err, bvp) {
        extractedBundlePath = bvp;
        callback(err);
        return;
      });
    },

    // Make sure this version doesn't exist
    function(callback) {
      path.exists(instanceVersionPath, function(exists) {
        var err = null;
        if (exists) {
          err = new Error(sprintf('Instance \'%s\' already has version \'%s\'', self.name, version));
        }
        callback(err);
        return;
      });
    },

    // Make sure an extracted bundle exists for the requested version
    function(callback) {
      fs.stat(extractedBundlePath, function(err, stats) {
        if (!err && !stats.isDirectory()) {
          err = new Error('No bundle for version \'' + version + '\'');
        }
        callback(err);
        return;
      });
    },

    // Create the directory for the target version
    function(callback) {
      fsutil.ensureDirectory(instanceVersionPath, callback);
    },

    // Retrieve the manifest object
    function(callback) {
      var manifestPath = path.join(extractedBundlePath, manifestConstants.MANIFEST_FILENAME);
      manifest.getManifestObject(manifestPath, true, function(err, _manifestObj) {
        manifestObj = _manifestObj;
        ignoredPaths = manifestObj['template_files'].concat(manifestObj['data_files']);
        callback(err);
        return;
      });
    },

    // Mirror the directory structure from the extracted bundle in the instance directory
    function(callback) {
      fsutil.treeToTemplate(extractedBundlePath, ignoredPaths, function(err, templateObject) {
        if (err) {
          callback(err);
          return;
        }

        // No directories
        if (Object.keys(templateObject).length === 0) {
          callback();
          return;
        }

        fsutil.templateToTree(instanceVersionPath, templateObject, true, callback);
      });
    },

    // Realize (render and save) templates
    function(callback) {
      var instanceData = deployTemplates.getInstanceTemplateObject(self.name, instanceVersionPath);
      deployTemplates.realizeApplicationTemplates(manifestObj, instanceData, extractedBundlePath,
                                                     instanceVersionPath, callback);
    },

    // Hard-link all the files (except template and data files)
    function(callback) {
      fsutil.hardLinkFiles(extractedBundlePath, instanceVersionPath, ignoredPaths, callback);
    },

    // Resolve the data files
    function(callback) {
      var instanceDataRoot = path.join(self.root, 'data');
      deployFiles.resolveDataFiles(extractedBundlePath, instanceDataRoot, instanceVersionPath,
                                      manifestObj['data_files'], callback);
    },

    // Execute the 'post_prepare' hook
    function(callback) {
      var hook = new hooks.InstanceHook('post', 'post_prepare',
                                        instanceVersionPath, false, hookEnv);
      hook.execute(null, [version], callback);
    }
  ], callback);
};

/**
 * Point the 'current' symlink to the specified version. Will verify
 * that the specified version exists before taking action.
 *
 * @param {String} version    The version to activate.
 * @param {Function} callback A callback fired with (err).
 */
Instance.prototype.activateVersion = function(version, callback) {
  var self = this;
  var newVersionPath;

  var newVersionLink = path.join(self.root, 'new');
  var currentVersionLink = path.join(self.root, 'current');
  var hookEnv = { 'CAST_INSTANCE_NAME': self.name };

  async.series([
    // Get the path to the specified version
    function(callback) {
      self.getVersionPath(version, function(err, vp) {
        newVersionPath = vp;
        callback(err);
        return;
      });
    },

    // Make sure the version exists
    function(callback) {
      path.exists(newVersionPath, function(exists) {
        var err = null;
        if (!exists) {
          err = new Error('Cannot activate nonexistent version \'' + version + '\'');
        }

        callback(err);
        return;
      });
    },

    function(callback) {
      var hook = new hooks.InstanceHook('post', 'pre_version_activate',
                                        newVersionPath, false, hookEnv);
      hook.execute(null, [version, newVersionPath], callback);
    },

    // Create the new link
    function(callback) {
      fs.symlink(path.resolve(newVersionPath), newVersionLink, callback);
    },

    // Atomically move it into place
    async.apply(fs.rename, newVersionLink, currentVersionLink),

    function(callback) {
      var hook = new hooks.InstanceHook('post', 'post_version_activate',
                                        newVersionPath, false, hookEnv);
      hook.execute(null, [version, newVersionPath], callback);
    }
  ], callback);
};

/**
 * Create an instance with a specified name for the specified bundle. The
 * instance will initially use the specified version of the bundle.
 *
 * @param {String} instanceName  A name to give to the instance.
 * @param {String} bundleName    The name of the bundle to use.
 * @param {String} bundleVersion The version of the bundle to use.
 * @param {Function} callback     A callback fired with (err).
 */
exports.createInstance = function(instanceName, bundleName, bundleVersion, callback) {
  if (!INSTANCE_NAME_RE.exec(instanceName)) {
    callback(new Error('Invalid instance name'));
    return;
  }

  var manifestObj, ignoredPaths, instance;
  var bundleNameFull = bundleName + '@' + bundleVersion;

  // Get the paths to the extracted bundle and the manifest
  var extractedBundleRoot = path.join(config.get()['extracted_dir'], bundleName);
  var extractedBundlePath = path.join(extractedBundleRoot, bundleNameFull);

  var instancePath = path.join(config.get()['app_dir'], instanceName);
  var instanceDataRoot = path.join(instancePath, 'data');
  var instanceVersionsRoot = path.join(instancePath, 'versions');
  var instanceBundleLink = path.join(instancePath, 'bundle');
  var instanceCurrentLink = path.join(instancePath, 'current');
  var instanceVersionPath = path.join(instanceVersionsRoot, bundleNameFull);

  async.series([
    // Make sure that the extracted bundle path exists
    function(callback) {
      path.exists(extractedBundlePath, function(exists) {
        if (!exists) {
          callback(new Error('Invalid bundle name or version'));
          return;
        }

        callback();
      });
    },

    // Make sure there isn't already an instance with this name
    function(callback) {
      path.exists(instancePath, function(exists) {
        if (exists) {
          callback(new Error('Instance name already in use'));
          return;
        }
        callback();
      });
    },

    // Create the instance directory
    function(callback) {
      fsutil.ensureDirectory(instancePath, function(err) {
        if (!err) {
          instance = new Instance(instanceName);
        }
        callback(err);
      });
    },

    // Create the data and versions directories
    async.apply(fsutil.ensureDirectory, instanceDataRoot),
    async.apply(fsutil.ensureDirectory, instanceVersionsRoot),

    // Create the bundle symlink
    async.apply(fs.symlink, path.resolve(extractedBundleRoot), instanceBundleLink),

    // Prepare the specified version
    function(callback) {
      instance.prepareVersion(bundleVersion, callback);
    },

    // Retrieve the manifest object
    function(callback) {
      var manifestPath = path.join(extractedBundlePath, manifestConstants.MANIFEST_FILENAME);
      manifest.getManifestObject(manifestPath, true, function(err, _manifestObj) {
        manifestObj = _manifestObj;
        callback(err);
        return;
      });
    },

    // Create a runit service
    function(callback) {
      deployServices.createService(instanceName, instanceVersionPath, manifestObj, callback);
    },

    // Symlink instace bundle version to "current"
    function(callback) {
      instance.activateVersion(bundleVersion, callback);
    }
  ],

  function(error) {
    if (error && instance) {
      instance.destroy(function() {
        callback(error);
        return;
      });
    }
    else {
      callback(error);
    }
  });
};

/**
 * Get an Instance object for an instance whose existence is verified.
 *
 * @param {String} instanceName  The name of the instance to retrieve.
 * @param {Function} callback     A callback that takes (err, instance).
 */
exports.getInstance = function(instanceName, callback) {
  var instance = new Instance(instanceName);
  instance.exists(function(exists) {
    if (!exists) {
      callback(new Error('Instance doesn\'t exist'));
      return;
    }
    else {
      callback(null, instance);
      return;
    }
  });
};

/**
 * Get a list of all instances (in the form of Instance objects).
 *
 * @param {Function} callback A callback that takes (err, instances).
 */
exports.getInstanceList = function(callback) {
  fs.readdir(config.get()['app_dir'], function(err, files) {
    if (err) {
      callback(err);
      return;
    }

    // Construct an Instance for each file in the app_dir
    var instanceList = files.map(function(file) {
      return new Instance(file);
    });

    // Make sure each instance actually exists (ie, isn't a non-directory of some sort)
    async.filter(instanceList, function(instance, callback) {
      instance.exists(callback);
    },
    function(instances) {
      callback(null, instances);
      return;
    });
  });
};

exports.Instance = Instance;
<|endoftext|>