Source: lib/hypertable/SerializedCellsWriter.js

/*
 * Copyright (C) 2007-2015 Hypertable, Inc.
 *
 * This file is part of Hypertable.
 *
 * Hypertable is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 3
 * of the License.
 *
 * Hypertable is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

'use strict';
var assert = require('assert');
var SerializedCellsFlag = require('./SerializedCellsFlag.js');
var SerializedCellsVersion = require('./SerializedCellsVersion.js');
var Timestamp = require('./Timestamp.js');
var Hypertable = require('./Client_types.js');

/**
 * Used for encoding cells into a serialized cells buffer.
 * @example
 * var writer = new hypertable.SerializedCellsWriter(1024);
 * var ts = new Timestamp( new Date(2015, 3, 15, 16, 15, 30) );
 * writer.add('row1', 'column1', 'qaulifier1', ts, 'value1');
 * writer.add('row2', 'column2', 'qaulifier2', ts, 'value2');
 * writer.add('row3', 'column3', 'qaulifier3', ts, 'value3');
 * ...
 * @param {Number} size Size of serialized cells buffer to generate
 * @param {Boolean} [grow=false] Grow buffer to accomodate added cells
 * @class
 * @classdesc Encodes cells into a serialized cells buffer.
 * @memberof module:hypertable
 */
var SerializedCellsWriter = module.exports = function(size, grow) {

  /**
   * Serialized cells buffer.
   * @name module:hypertable.SerializedCellsWriter#buffer
   * @type Buffer
   */

  /**
   * Current encode offset within {@link module:hypertable.SerializedCellsWriter#buffer buffer}.
   * @name module:hypertable.SerializedCellsWriter#offset
   * @type Number
   */

  if (size === undefined)
    throw new TypeError('Missing \'size\' argument in SerializedCellsWriter constructor');
  if (typeof size !== 'number')
    throw new TypeError('\'size\' argument for SerializedCellsWriter constructor must be of type \'number\'');
  if (grow !== undefined && typeof grow !== 'boolean')
    throw new TypeError('\'grow\' argument for SerializedCellsWriter constructor must be of type \'boolean\'');
  this.buffer = new Buffer(size);
  this.offset = 0;
  this.finalized = false;
  this.grow = Boolean(grow);
  this.previousRow = null;
};

SerializedCellsWriter.prototype = {};

SerializedCellsWriter.prototype.constructor = SerializedCellsWriter;

SerializedCellsWriter.prototype.growBy = function(length) {
  if (!this.buffer)
    this.buffer = new Buffer(length);
  else {
    var tmpBuffer = new Buffer(this.buffer.length+length);
    this.buffer.copy(tmpBuffer, 0, 0, this.offset);
    this.buffer = tmpBuffer;
  }
};

/**
 * Adds a cell to the serialized cells buffer.
 * This method can be called with either a single {@link Cell} argument, or all of the cell
 * components, for example:
 * <pre>
 * writer.add(cell);
 * writer.add(row, column_family, column_qualifier, timestamp, value, cell_flag);
 * </pre>
 * @param cell|row {module:hypertable.Cell|String} Cell or row key
 * @param [column_family] {String} Column family
 * @param [column_qualifier] {String} Column qualifier
 * @param [timestamp] {Timestamp|Int64} Timetamp
 * @param [value] {Buffer} Value
 * @param [cell_flag=255] {Number} Cell flag
 */
SerializedCellsWriter.prototype.add = function(row, column_family, column_qualifier, timestamp, value, cell_flag) {

  var rowBuffer;
  var columnFamilyBuffer;
  var columnQualifierBuffer;
  var valueBuffer;

  if (row instanceof Cell) {
    rowBuffer = new Buffer(row.key.row);
    columnFamilyBuffer = row.key.column_family ? new Buffer(row.key.column_family) : undefined;
    columnQualifierBuffer = row.key.column_qualifier ? new Buffer(row.key.column_qualifier) : undefined;
    valueBuffer = row.value;
    timestamp = row.key.timestamp;
    cell_flag = row.key.flag;
  }
  else {

    assert(typeof row === 'string',
           'Parameter \'row\' must be of \'string\' type');
    if (column_family)
      assert(typeof column_family === 'string',
             'Parameter \'column_family\' must be of \'string\' type');
    if (column_qualifier)
      assert(typeof column_qualifier === 'string',
             'Parameter \'column_family\' must be of \'string\' type');

    rowBuffer = new Buffer(row);
    columnFamilyBuffer = column_family ? new Buffer(column_family) : undefined;
    columnQualifierBuffer = column_qualifier ? new Buffer(column_qualifier) : undefined;

    if (value) {
      if (typeof value === 'string')
        valueBuffer = new Buffer(value);
      else
        valueBuffer = value;
    }

    if (cell_flag === undefined)
      cell_flag = Hypertable.KeyFlag.INSERT;
  }

  var needRow = true;

  if (this.previousRow && rowBuffer.equals(this.previousRow))
    needRow = false;

  var length = 13;
  if (columnFamilyBuffer)
    length += columnFamilyBuffer.length;
  if (columnQualifierBuffer)
    length += columnQualifierBuffer.length;
  if (valueBuffer)
    length += valueBuffer.length;
  if (this.offset === 0)
    length += 4;
  if (needRow)
    length += rowBuffer.length;

  var flag;

  if (timestamp === undefined || timestamp == null || Timestamp.AUTO_ASSIGN.equals(timestamp))
    flag = SerializedCellsFlag.AUTO_TIMESTAMP;
  else if (!Timestamp.TIMESTAMP_NULL.equals(timestamp)) {
    flag = SerializedCellsFlag.HAVE_TIMESTAMP;
    length += 8;
  }

  // need to leave room for the termination byte
  if (!this.buffer || length > this.buffer.length - this.offset) {
    if (this.buffer && this.grow) {
      this.growBy(length);
    }
    else {
      if (this.offset !== 0)
        return false;
      this.buffer = new Buffer(length);
    }
  }
  
  // version
  if (this.offset === 0) {
    this.buffer.writeUInt32LE(SerializedCellsVersion.SCVERSION, this.offset);
    this.offset += 4;
  }

  // flag byte
  this.buffer[this.offset] = flag;
  this.offset++;

  // timestamp
  if (flag === SerializedCellsFlag.HAVE_TIMESTAMP) {
    if (timestamp instanceof Timestamp)
      timestamp.copyLE(this.buffer, this.offset);
    else {
      var tmpTimestamp = new Timestamp(timestamp);
      tmpTimestamp.copyLE(this.buffer, this.offset);
    }
    this.offset += 8;
  }

  // revision
  if (flag === SerializedCellsFlag.HAVE_REVISION &&
      (flag & SerializedCellsFlag.REV_IS_TS) === 0) {
    this.buffer.writeUInt32LE(0, this.offset);
    this.offset += 4;
    this.buffer.writeUInt32LE(0, this.offset);
    this.offset += 4;
  }

  // row; only write it if it's not identical to the previous row
  if (needRow) {
    rowBuffer.copy(this.buffer, this.offset);
    this.offset += rowBuffer.length;
    this.previousRow = rowBuffer;
  }
  this.buffer[this.offset] = 0;
  this.offset++;

  if (columnFamilyBuffer) {
    columnFamilyBuffer.copy(this.buffer, this.offset);
    this.offset += columnFamilyBuffer.length;
  }
  this.buffer[this.offset] = 0;
  this.offset++;

  if (columnQualifierBuffer) {
    columnQualifierBuffer.copy(this.buffer, this.offset);
    this.offset += columnQualifierBuffer.length;
  }
  this.buffer[this.offset] = 0;
  this.offset++;

  if (valueBuffer) {
    this.buffer.writeUInt32LE(valueBuffer.length, this.offset);
    this.offset += 4;
    valueBuffer.copy(this.buffer, this.offset);
    this.offset += valueBuffer.length;
  }
  else {
    this.buffer.writeUInt32LE(0, this.offset);
    this.offset += 4;
  }
  this.buffer[this.offset] = cell_flag;
  this.offset++;

  return true;
}

SerializedCellsWriter.prototype.finalize = function(flag) {

  if (!this.buffer)
    this.buffer = new Buffer(5);

  if (this.offset === 0) {
    if (this.buffer.length < 5) {
      assert(this.grow, 'No room for finalize flag in SerializedCellsWriter');
      this.growBy(5);
    }
    this.buffer.writeUInt32LE(SerializedCellsVersion.SCVERSION, this.offset);
    this.offset += 4;
  }

  flag = flag || 0;
  this.buffer[this.offset] = SerializedCellsFlag.EOB | flag;
  this.offset++;

  this.finalized = true;
}


/**
 * Gets serialized cells buffer.  Finalizes the buffer and returns it sliced to
 * the exact length.
 * @return Serialized cells buffer
 */
SerializedCellsWriter.prototype.getBuffer = function() {
  if (!this.finalized)
    this.finalize();
  return this.buffer.slice(0, this.offset);
}

/**
 * Checks if serialized cells buffer is empty
 * ({@link module:hypertable.SerializedCellsWriter#offset offset} is 0)
 * @return <i>true</i> if buffer is empty, <i>false</i> otherwise.
 */
SerializedCellsWriter.prototype.empty = function() {
  return this.offset === 0;
}

/**
 * Clears serialized cells buffer.  This method sets the object to
 * uninitialized state, setting
 * {@link module:hypertable.SerializedCellsWriter#offset offset} to 0, but keeping
 * the underlying {@link module:hypertable.SerializedCellsWriter#buffer buffer}.
 */
SerializedCellsWriter.prototype.clear = function() {
  this.offset = 0;
  this.previousRow = undefined;
  this.finalized = false;
}