@sap/odata-v4
Version:
OData V4.0 server library
231 lines (193 loc) • 8.07 kB
JavaScript
'use strict';
const Reader = require('./Reader');
const DeserializationError = require('../errors/DeserializationError');
/**
* The dataReader can read up to a stop pattern, a given count of bytes and untill no more payload is available
* @enum {{STOP_PATTERN: string, BYTE_COUNT: string, TILL_END: string}}
*/
const STOP_CONDITION = {
STOP_PATTERN: 'STOP_PATTERN', // Read until stop pattern, after this the caches read position points to begin of stop pattern
BYTE_COUNT: 'BYTE_COUNT', // Read a given count of bytes, after this the read position points to the first byte after the read bytes
TILL_END: 'TILL_END' // Read until there is no more input data, see "last" parameter on method readCache (avoids search and byte counting)
};
/**
* States
* @type {{READ_NEXT_CHUNK: number, FINISHED: number}}
*/
const STATES = {
READ_NEXT_CHUNK: 0, // data reader consumes chunks from cache
FINISHED: 1 // data reader finished (e.g. pattern, length or end of input reached)
};
/**
* Events
* @type {string}
* @enum {{DATA: string, END: string}}
*/
const EVENTS = {
DATA: 'data', // data reader consumed data from cache
END: 'end' // data reader reached state FINISHED
};
/**
* The data reader reads bytes from the cache until the stop condition is reached. If the cache is empty
* and the data reader needs more data, the readCache function returns to the caller signaling that more data is
* required.
*
* There are three available conditions:
* STOP_PATTERN: Reader consumed bytes from cache until the stop pattern is reached
* BYTE_COUNT: Reader consumes a given amount of bytes from the cache
* TILL_END: Reader consumes until the caller signals that the end of data is reached
*
* The Following events are emitted through an injected emitter (the prefix is also injected)
* <prefix>.data: For every consumed chunk
* <prefix>.end: When the reader reaches state finished
*
* @extends Reader
* */
class DataReader extends Reader {
/**
* Factory for automatic creation depending on content-type
*
* @param {ContentTypeInfo} contentTypeInfo - Content type which caused using this reader
* (here not used, but still in interface due to generic caller)
* @param {Object} headers - Header information (currently only the content-length header is evaluated)
* @returns {DataReader}
*/
static createInstance(contentTypeInfo, headers) {
return new DataReader(headers);
}
/**
* Constructor
*
* @param {Object} headers - Header information (currently only the content-length header is evaluated)
*/
constructor(headers) {
super();
/**
* @enum {{STOP_PATTERN: string, BYTE_COUNT: string, TILL_END: string}}
* @private
*/
this._stopCondition = STOP_CONDITION.TILL_END; // default
/**
* Store content-length for STOP_CONDITION.BYTE_COUNT
* @type {number}
* @private
*/
this._needToRead = null;
// Check for content-length header, please not that these headers are not the nodejs generated headers
if (headers) {
const contentLength = headers['content-length'];
if (contentLength) {
this._contentLength = Number.parseInt(contentLength.getValue(), 10);
this._needToRead = this._contentLength;
this._stopCondition = STOP_CONDITION.BYTE_COUNT;
}
}
this._state = STATES.READ_NEXT_CHUNK;
}
/**
* Sets the stop pattern and sets the mode to STOP_PATTERN
*
* @param {string} stopPattern
*/
setStopPattern(stopPattern) {
this._stopPattern = stopPattern;
this._stopCondition = STOP_CONDITION.STOP_PATTERN; // default
}
/**
* Consume data from cache. If the cache is empty and more data is required, this is signaled and readCache is called again. The state is preserved.
*
* @param {Reader} reader - Current reader
* @param {Cache} cache - Cache containing data for processing
* @param {boolean} last - Last call, no more date available
* @returns {boolean}
* this: this reader needs more data and caller should call this method again with more data in cache
* false: this reader is finished caller should pop this reader from stack
* null: new sub reader is on stack, call this method again after the sub reader is finished
*/
readCache(reader, cache, last) {
let needMoreData = false;
while (!needMoreData && this._state !== STATES.FINISHED) {
switch (this._stopCondition) {
case STOP_CONDITION.STOP_PATTERN:
needMoreData = this._readToStopPattern(cache, last);
break;
case STOP_CONDITION.BYTE_COUNT:
needMoreData = this._readAmount(cache, last);
break;
default:
// STOP_CONDITION.TILL_END:
needMoreData = this._readUntilEndOfInput(cache, last);
break;
}
}
if (this._state === STATES.FINISHED) {
this.emit(EVENTS.END);
return false;
}
if (last && needMoreData) {
throw new DeserializationError('More data required to process multipart');
}
return needMoreData;
}
/**
* Read data until all input data is consumed
*
* @param {Cache} cache - Cache containing data for processing
* @param {boolean} last - Last call, no more data available
* @returns {boolean} - true if more data is required, false if no more data is required
*/
_readUntilEndOfInput(cache, last) {
const consumedBytes = cache.getLength() - cache.getReadPos();
this.emitAndConsume(cache, consumedBytes, EVENTS.DATA);
if (last) {
this._state = STATES.FINISHED;
return false;
}
return true;
}
/**
* Read data until the stop pattern is reached. The read position is set to the beginning of the stop pattern.
* The caller which starts the body reader is responsible for reading the stop pattern.
*
* @param {Cache} cache - Cache containing data for processing
* @returns {boolean} - true if more data is required, false if no more data is required
*/
_readToStopPattern(cache) {
if (cache.getLength() - cache.getReadPos() < this._stopPattern.length) {
return true;
}
const pos = cache.indexOf(this._stopPattern, cache.getSearchPosition());
if (pos === -1) {
// stop pattern not found
const consumed = cache.getLength() - cache.getReadPos() - this._stopPattern.length;
this.emitAndConsume(cache, consumed, EVENTS.DATA);
return true;
}
// stop pattern found
const consumed = pos - cache.getReadPos();
this.emitAndConsume(cache, consumed, EVENTS.DATA);
this._state = STATES.FINISHED;
return false;
}
/**
* Read data until a given amount of bytes are read.
*
* @param {Cache} cache - Cache containing data for processing
* @returns {boolean} - true if more data is required, false if no more data is required
*/
_readAmount(cache) {
const available = cache.getLength() - cache.getReadPos();
if (available < this._needToRead) {
this.emitAndConsume(cache, available, EVENTS.DATA);
this._needToRead -= available;
return true;
}
// there are more byte in buffer than needed
this.emitAndConsume(cache, this._needToRead, EVENTS.DATA);
this._needToRead = 0;
this._state = STATES.FINISHED;
return false;
}
}
DataReader.EVENTS = EVENTS;
module.exports = DataReader;