Source: lib/hypertable/SerializedCellsReader.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 Int64 = require('node-int64');
var Hypertable = require('./Client_types.js');

/**
 * Used for decoding cells from a serialized cells buffer.
 * @example
 * var reader = new hypertable.SerializedCellsReader(buffer);
 * while (reader.next()) {
 *   var cell = reader.getCell();
 *   // do something with cell ...
 * }
 * @param {Buffer} buffer Buffer holding serialized cells
 * @class
 * @classdesc Decodes cells from a serialized cells buffer.
 * @memberof module:hypertable
 */
var SerializedCellsReader = module.exports = function(buffer) {

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

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

  if (buffer === undefined)
    throw new TypeError('Missing \'buffer\' argument in SerializedCellsReader constructor');
  if (!(buffer instanceof Buffer))
    throw new TypeError('\'buffer\' argument for SerializedCellsReader constructor must be a Buffer');
  this.buffer = buffer;
  this.offset = 0;
  this.previousRow = null;
  this.flag = 0;
  this.timestamp = null;
  this.revision = null;
  this.row = null;
  this.columnFamily = null;
  this.columnQualifier = null;
  this.value = null;
  this.cellFlag = 0;

  var version = this.buffer.readUInt32LE(this.offset);
  this.offset += 4;
  if (version !== SerializedCellsVersion.SCVERSION)
    throw new Error('Invalid buffer passed to SerializedCellsReader (expected version ' +
                    SerializedCellsVersion.SCVERSION + ', but got ' + version);
};


SerializedCellsReader.prototype = {};

SerializedCellsReader.prototype.constructor = SerializedCellsReader;

/**
 * Extracts string from buffer.  Advances offset to byte position after
 * extracted string.
 * @return {String} Extracted string
 * @private
 */
SerializedCellsReader.prototype.extractStringBuffer = function() {
  var startOffset = this.offset;
  while (this.offset < this.buffer.length && this.buffer[this.offset] !== 0)
    this.offset++;
  if (this.offset == this.buffer.length)
    throw new Error('Unterminated string encounterd in serialized cells buffer');
  var strBuf;
  if (this.offset > startOffset)
    strBuf = this.buffer.slice(startOffset, this.offset);
  this.offset++;
  return strBuf;
};

/**
 * Flips <code>length</code> bytes of {@link module:hypertable.SerializedCellsReader#buffer buffer}
 * starting at {@link module:hypertable.SerializedCellsReader#offset offset}.  For example:
 * <pre>
 * 12345678 -> 87654321
 * </pre>
 * @param {Number} length Length of bytes to flip
 */
SerializedCellsReader.prototype.flipBytes = function(length) {
  for (var i=0; i<length/2; i++) {
    var tmp = this.buffer[(this.offset+length)-i-1];
    this.buffer[(this.offset+length)-i-1] = this.buffer[this.offset+i];
    this.buffer[this.offset+i] = tmp;
  }
};

/**
 * Extracts next cell from {@link module:hypertable.SerializedCellsReader#buffer buffer}.  Advances
 * {@link module:hypertable.SerializedCellsReader#offset offset} to first byte past extracted cell.
 * The extracted cell, or portion thereof, can be subsequently obtain with one
 * of the following methods:
 * <p/>
 * <table cellpadding="3">
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getCell getCell()}</td><td>Cell</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getKey getKey()}</td><td>Key</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getRow getRow()}</td><td>Row</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getColumnFamily getColumnFamily()}</td><td>Column family</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getColumnQualifier getColumnQualifier()}</td><td>Column qualifier</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getTimestamp getColumnTimestamp()}</td><td>Timestamp</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getRevision getColumnRevision()}</td><td>Revision</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getValue getValue()}</td><td>Value</td></tr>
 * <tr><td>{@link module:hypertable.SerializedCellsReader#getCellFlag getCellFlag()}</td><td>Cell flag</td></tr>
 * </table>
 * @return {Boolean} <i>true</i> if cell successfully extracted, <i>false</i> if
 * end of buffer.
 */
SerializedCellsReader.prototype.next = function() {

  if (this.offset >= this.buffer.length)
    return false;

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

  // Check for EOB
  if (this.flag & SerializedCellsFlag.EOB) {
    this.offset = this.buffer.length;
    return false;
  }

  // Timestamp
  if (this.flag & SerializedCellsFlag.HAVE_TIMESTAMP) {
    this.flipBytes(8);
    this.timestamp = new Int64(this.buffer, this.offset);
    this.offset += 8;
  }

  // Revision
  if (this.flag & SerializedCellsFlag.REV_IS_TS)
    this.revision = this.timestamp;
  else if (this.flag & SerializedCellsFlag.HAVE_REVISION) {
    this.flipBytes(8);
    this.revision = new Int64(this.buffer, this.offset);
    this.offset += 8;
  }
  else
    this.revision = null;

  // Row
  if (this.buffer[this.offset] === 0) {
    assert(this.previousRow, 'Mal-formed serialized cells buffer');
    this.row = this.previousRow;
    this.offset++;
  }
  else {
    this.row = this.previousRow = this.extractStringBuffer();
    assert(this.row, 'Empty row key encountered in serialized cells buffer');
  }

  // Column family
  this.columnFamily = this.extractStringBuffer();
  assert(this.columnFamily, 'Empty row key encountered in serialized cells buffer');

  // Column Qualifier
  this.columnQualifier = this.extractStringBuffer();

  // Value
  var valueLength = this.buffer.readUInt32LE(this.offset);
  this.offset += 4;
  assert(this.offset + valueLength < this.buffer.length,
         'Mal-formed value encountered in serialized cells buffer');
  if (valueLength === 0)
    this.value = null;
  else {
    this.value = this.buffer.slice(this.offset, this.offset+valueLength);
    this.offset += valueLength;
  }
  
  // Cell flag
  this.cellFlag = this.buffer[this.offset];
  this.offset++;
  if (this.cellFlag === Hypertable.KeyFlag.DELETE_ROW)
    this.columnFamily = this.columnQualifier = null;
  else if (this.cellFlag === Hypertable.KeyFlag.DELETE_CF)
    this.columnQualifier = null;

  return true;
};

/**
 * Returns row of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {String} Row of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getRow = function() {
  assert(this.row,
         'SerializedCellsReader.next() must first be called before calling this method');
  return this.row.toString();
};

/**
 * Returns column family of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {String} Column family of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getColumnFamily = function() {
  if (!this.columnFamily)
    return '';
  return this.columnFamily.toString();
};

/**
 * Returns column qualifier of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {String} Column qualifier of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getColumnQualifier = function() {
  if (!this.columnQualifier)
    return '';
  return this.columnQualifier.toString();
};

/**
 * Returns timestamp of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {Int64} Timestamp of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getTimestamp = function() {
  return this.timestamp;
};

/**
 * Returns revision of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {Int64} Revision of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getRevision = function() {
  return this.revision;
};

/**
 * Returns value of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {Buffer} Value of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getValue = function() {
  return this.value;
};

/**
 * Returns cell flag of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {Number} Cell flag of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getCellFlag = function() {
  return this.cellFlag;
};

/**
 * Returns key of cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {module:hypertable.Key} Key of cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getKey = function() {
  return new Hypertable.Key({row: this.getRow(),
                             column_family: this.getColumnFamily(),
                             column_qualifier: this.getColumnQualifier(),
                             timestamp: this.timestamp,
                             revision: this.revision,
                             flag: this.cellFlag});
};

/**
 * Returns cell extracted from last call to {@link SerializedCellsReader#next next}.
 * @return {module:hypertable.Cell} Cell extracted from last call to {@link SerializedCellsReader#next next}.
 */
SerializedCellsReader.prototype.getCell = function() {
  return new Hypertable.Cell({key: this.getKey(), value: this.value});
};