| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF 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. | |
| */ | |
| 'use strict'; | |
| import * as Util from "util"; | |
| import {ComplexObjectType, CompositeType, ObjectType} from "./ObjectType"; | |
| import BinaryUtils from "./internal/BinaryUtils"; | |
| import BinaryType, {BinaryField, BinaryTypeBuilder} from "./internal/BinaryType"; | |
| import ArgumentChecker from "./internal/ArgumentChecker"; | |
| import MessageBuffer from "./internal/MessageBuffer"; | |
| import Logger from "./internal/Logger"; | |
| import { IgniteClientError } from "./Errors"; | |
| import { PRIMITIVE_TYPE } from "./internal/Constants"; | |
| import BinaryCommunicator from "./internal/BinaryCommunicator"; | |
| const HEADER_LENGTH = 24; | |
| const VERSION = 1; | |
| // user type | |
| const FLAG_USER_TYPE = 0x0001; | |
| // schema exists | |
| const FLAG_HAS_SCHEMA = 0x0002; | |
| // object contains raw data | |
| const FLAG_HAS_RAW_DATA = 0x0004; | |
| // offsets take 1 byte | |
| const FLAG_OFFSET_ONE_BYTE = 0x0008; | |
| // offsets take 2 bytes | |
| const FLAG_OFFSET_TWO_BYTES = 0x0010; | |
| // compact footer, no field IDs | |
| const FLAG_COMPACT_FOOTER = 0x0020; | |
| /** | |
| * Class representing a complex Ignite object in the binary form. | |
| * | |
| * It corresponds to COMPOSITE_TYPE.COMPLEX_OBJECT {@link ObjectType.COMPOSITE_TYPE}, | |
| * has mandatory type Id, which corresponds to a name of the complex type, | |
| * and includes optional fields. | |
| * | |
| * An instance of the BinaryObject can be obtained/created by the following ways: | |
| * - returned by the client when a complex object is received from Ignite cache | |
| * and is not deserialized to another JavaScript object. | |
| * - created using the public constructor. Fields may be added to such an instance using setField() method. | |
| * - created from a JavaScript object using static fromObject() method. | |
| */ | |
| export class BinaryObject { | |
| private _typeBuilder: BinaryTypeBuilder; | |
| private _modified: boolean; | |
| private _hasSchema: boolean; | |
| private _compactFooter: boolean; | |
| private _hasRawData: boolean; | |
| private _fields: Map<number, BinaryObjectField>; | |
| private _buffer: MessageBuffer; | |
| private _startPos: number; | |
| private _hashCode: number; | |
| private _schemaOffset: number; | |
| private _length: number; | |
| private _offsetType: PRIMITIVE_TYPE; | |
| /** | |
| * Creates an instance of the BinaryObject without any fields. | |
| * | |
| * Fields may be added later using setField() method. | |
| * | |
| * @param {string} typeName - name of the complex type to generate the type Id. | |
| * | |
| * @return {BinaryObject} - new BinaryObject instance. | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| constructor(typeName: string) { | |
| ArgumentChecker.notEmpty(typeName, 'typeName'); | |
| this._buffer = null; | |
| this._fields = new Map<number, BinaryObjectField>(); | |
| this._typeBuilder = BinaryTypeBuilder.fromTypeName(typeName); | |
| this._modified = false; | |
| this._schemaOffset = null; | |
| this._hasSchema = false; | |
| this._compactFooter = false; | |
| this._hasRawData = false; | |
| this._hashCode = null; | |
| } | |
| /** | |
| * Creates an instance of the BinaryObject from the specified instance of JavaScript Object. | |
| * | |
| * All fields of the JavaScript Object instance with their values are added to the BinaryObject. | |
| * Fields may be added or removed later using setField() and removeField() methods. | |
| * | |
| * If complexObjectType parameter is specified, then the type Id is taken from it. | |
| * Otherwise, the type Id is generated from the name of the JavaScript Object. | |
| * | |
| * @async | |
| * | |
| * @param {object} jsObject - instance of JavaScript Object | |
| * which adds and initializes the fields of the BinaryObject instance. | |
| * @param {ComplexObjectType} [complexObjectType] - instance of complex type definition | |
| * which specifies non-standard mapping of the fields of the BinaryObject instance | |
| * to/from the Ignite types. | |
| * | |
| * @return {BinaryObject} - new BinaryObject instance. | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| static async fromObject(jsObject, complexObjectType = null) { | |
| ArgumentChecker.notEmpty(jsObject, 'jsObject'); | |
| ArgumentChecker.hasType(complexObjectType, 'complexObjectType', false, ComplexObjectType); | |
| const typeBuilder = BinaryTypeBuilder.fromObject(jsObject, complexObjectType); | |
| const result = new BinaryObject(typeBuilder.getTypeName()); | |
| result._typeBuilder = typeBuilder; | |
| let fieldName; | |
| for (let field of result._typeBuilder.getFields()) { | |
| fieldName = field.name; | |
| if (jsObject && jsObject[fieldName] !== undefined) { | |
| result.setField( | |
| fieldName, | |
| jsObject[fieldName], | |
| complexObjectType ? complexObjectType._getFieldType(fieldName) : null); | |
| } | |
| else { | |
| throw IgniteClientError.serializationError( | |
| true, Util.format('field "%s" is undefined', fieldName)); | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Sets the new value of the specified field. | |
| * Adds the specified field, if it did not exist before. | |
| * | |
| * Optionally, specifies a type of the field. | |
| * If the type is not specified then during operations the Ignite client | |
| * will try to make automatic mapping between JavaScript types and Ignite object types - | |
| * according to the mapping table defined in the description of the {@link ObjectType} class. | |
| * | |
| * @param {string} fieldName - name of the field. | |
| * @param {*} fieldValue - new value of the field. | |
| * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} [fieldType] - type of the field: | |
| * - either a type code of primitive (simple) type | |
| * - or an instance of class representing non-primitive (composite) type | |
| * - or null (or not specified) that means the type is not specified. | |
| * | |
| * @return {BinaryObject} - the same instance of BinaryObject | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| setField(fieldName, fieldValue, fieldType = null) { | |
| ArgumentChecker.notEmpty(fieldName, 'fieldName'); | |
| this._modified = true; | |
| const field = new BinaryObjectField(fieldName, fieldValue, fieldType); | |
| this._fields.set(field.id, field); | |
| this._typeBuilder.setField(fieldName, field.typeCode); | |
| return this; | |
| } | |
| /** | |
| * Removes the specified field. | |
| * Does nothing if the field does not exist. | |
| * | |
| * @param {string} fieldName - name of the field. | |
| * | |
| * @return {BinaryObject} - the same instance of BinaryObject | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| removeField(fieldName) { | |
| ArgumentChecker.notEmpty(fieldName, 'fieldName'); | |
| this._modified = true; | |
| this._fields.delete(BinaryField._calculateId(fieldName)); | |
| this._typeBuilder.removeField(fieldName); | |
| return this; | |
| } | |
| /** | |
| * Checks if the specified field exists in this BinaryObject instance. | |
| * | |
| * @param {string} fieldName - name of the field. | |
| * | |
| * @return {boolean} - true if exists, false otherwise. | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| hasField(fieldName) { | |
| ArgumentChecker.notEmpty(fieldName, 'fieldName'); | |
| return this._fields.has(BinaryField._calculateId(fieldName)); | |
| } | |
| /** | |
| * Returns a value of the specified field. | |
| * | |
| * Optionally, specifies a type of the field. | |
| * If the type is not specified then the Ignite client | |
| * will try to make automatic mapping between JavaScript types and Ignite object types - | |
| * according to the mapping table defined in the description of the {@link ObjectType} class. | |
| * | |
| * @async | |
| * | |
| * @param {string} fieldName - name of the field. | |
| * @param {ObjectType.PRIMITIVE_TYPE | CompositeType} [fieldType] - type of the field: | |
| * - either a type code of primitive (simple) type | |
| * - or an instance of class representing non-primitive (composite) type | |
| * - or null (or not specified) that means the type is not specified. | |
| * | |
| * @return {*} - value of the field or JavaScript undefined if the field does not exist. | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| async getField(fieldName, fieldType = null) { | |
| ArgumentChecker.notEmpty(fieldName, 'fieldName'); | |
| const field = this._fields.get(BinaryField._calculateId(fieldName)); | |
| return field ? await field.getValue(fieldType) : undefined; | |
| } | |
| /** | |
| * Deserializes this BinaryObject instance into an instance of the specified complex object type. | |
| * | |
| * @async | |
| * | |
| * @param {ComplexObjectType} complexObjectType - instance of class representing complex object type. | |
| * | |
| * @return {object} - instance of the JavaScript object | |
| * which corresponds to the specified complex object type. | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| async toObject(complexObjectType) { | |
| ArgumentChecker.notNull(complexObjectType, 'complexObjectType'); | |
| ArgumentChecker.hasType(complexObjectType, 'complexObjectType', false, ComplexObjectType); | |
| const result = new (complexObjectType._objectConstructor); | |
| let binaryField; | |
| let fieldName; | |
| for (let field of this._fields.values()) { | |
| binaryField = this._typeBuilder.getField(field.id); | |
| if (!binaryField) { | |
| throw IgniteClientError.serializationError( | |
| false, Util.format('field with id "%s" can not be deserialized', field.id)); | |
| } | |
| fieldName = binaryField.name; | |
| result[fieldName] = await field.getValue(complexObjectType._getFieldType(fieldName)); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Returns type name of this BinaryObject instance. | |
| * | |
| * @return {string} - type name. | |
| */ | |
| getTypeName() { | |
| return this._typeBuilder.getTypeName(); | |
| } | |
| get fields() { | |
| return this._fields; | |
| } | |
| /** | |
| * Returns names of all fields of this BinaryObject instance. | |
| * | |
| * @return {Array<string>} - names of all fields. | |
| * | |
| * @throws {IgniteClientError} if error. | |
| */ | |
| getFieldNames() { | |
| return this._typeBuilder.schema.fieldIds.map(fieldId => { | |
| const field = this._typeBuilder.getField(fieldId); | |
| if (field) { | |
| return field.name; | |
| } | |
| else { | |
| throw IgniteClientError.internalError( | |
| Util.format('Field "%s" is absent in binary type fields', fieldId)); | |
| } | |
| }); | |
| } | |
| /** Private methods */ | |
| /** | |
| * @ignore | |
| */ | |
| static _isFlagSet(flags, flag) { | |
| return (flags & flag) === flag; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| static async _fromBuffer(communicator: BinaryCommunicator, buffer: MessageBuffer) { | |
| const result = new BinaryObject(new ComplexObjectType({}).typeName); | |
| result._buffer = buffer; | |
| result._startPos = buffer.position; | |
| await result._read(communicator); | |
| return result; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| async _getHashCode(communicator: BinaryCommunicator) { | |
| if (this._hashCode !== null && !this._modified) { | |
| return this._hashCode; | |
| } | |
| await this._write(communicator, new MessageBuffer()); | |
| return this._hashCode; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| _getTypeId() { | |
| return this._typeBuilder.getTypeId(); | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| async _write(communicator, buffer) { | |
| if (this._buffer && !this._modified) { | |
| buffer.writeBuffer(this._buffer.buffer, this._startPos, this._startPos + this._length); | |
| } | |
| else { | |
| await this._typeBuilder.finalize(communicator); | |
| this._startPos = buffer.position; | |
| buffer.position = this._startPos + HEADER_LENGTH; | |
| this._hasSchema = (this._fields.size > 0); | |
| if (this._hasSchema) { | |
| let field; | |
| // write fields | |
| for (field of this._fields.values()) { | |
| await field._writeValue(communicator, buffer, this._typeBuilder.getField(field.id).typeCode); | |
| } | |
| this._schemaOffset = buffer.position - this._startPos; | |
| this._offsetType = field.getOffsetType(this._startPos); | |
| // write schema | |
| for (let field of this._fields.values()) { | |
| field._writeOffset(buffer, this._startPos, this._offsetType); | |
| } | |
| } | |
| else { | |
| this._schemaOffset = 0; | |
| } | |
| this._length = buffer.position - this._startPos; | |
| this._buffer = buffer; | |
| // write header | |
| this._writeHeader(); | |
| this._buffer.position = this._startPos + this._length; | |
| this._modified = false; | |
| } | |
| if (Logger.debug) { | |
| Logger.logDebug('BinaryObject._write [' + [...this._buffer.getSlice(this._startPos, this._startPos + this._length)] + ']'); | |
| } | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| _writeHeader() { | |
| this._buffer.position = this._startPos; | |
| // type code | |
| this._buffer.writeByte(BinaryUtils.TYPE_CODE.COMPLEX_OBJECT); | |
| // version | |
| this._buffer.writeByte(VERSION); | |
| // flags | |
| let flags = FLAG_USER_TYPE; | |
| if (this._hasSchema) { | |
| flags = flags | FLAG_HAS_SCHEMA | FLAG_COMPACT_FOOTER; | |
| } | |
| if (this._offsetType === BinaryUtils.TYPE_CODE.BYTE) { | |
| flags = flags | FLAG_OFFSET_ONE_BYTE; | |
| } | |
| else if (this._offsetType === BinaryUtils.TYPE_CODE.SHORT) { | |
| flags = flags | FLAG_OFFSET_TWO_BYTES; | |
| } | |
| this._buffer.writeShort(flags); | |
| // type id | |
| this._buffer.writeInteger(this._typeBuilder.getTypeId()); | |
| // hash code | |
| this._hashCode = BinaryUtils.contentHashCode( | |
| this._buffer, this._startPos + HEADER_LENGTH, this._schemaOffset - 1); | |
| this._buffer.writeInteger(this._hashCode); | |
| // length | |
| this._buffer.writeInteger(this._length); | |
| // schema id | |
| this._buffer.writeInteger(this._hasSchema ? this._typeBuilder.getSchemaId() : 0); | |
| // schema offset | |
| this._buffer.writeInteger(this._schemaOffset); | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| async _read(communicator) { | |
| await this._readHeader(communicator); | |
| if (this._hasSchema) { | |
| this._buffer.position = this._startPos + this._schemaOffset; | |
| const fieldOffsets = []; | |
| const fieldIds = this._typeBuilder.schema.fieldIds; | |
| let index = 0; | |
| let fieldId; | |
| let schemaEndOffset = this._startPos + this._length; | |
| if (this._hasRawData) { | |
| schemaEndOffset -= BinaryUtils.getSize(BinaryUtils.TYPE_CODE.INTEGER); | |
| } | |
| while (this._buffer.position < schemaEndOffset) { | |
| if (!this._compactFooter) { | |
| fieldId = this._buffer.readInteger(); | |
| this._typeBuilder.schema.addField(fieldId); | |
| } | |
| else { | |
| if (index >= fieldIds.length) { | |
| throw IgniteClientError.serializationError( | |
| false, 'wrong number of fields in schema'); | |
| } | |
| fieldId = fieldIds[index]; | |
| index++; | |
| } | |
| fieldOffsets.push([fieldId, this._buffer.readNumber(this._offsetType, false)]); | |
| } | |
| fieldOffsets.sort((val1, val2) => val1[1] - val2[1]); | |
| let offset; | |
| let nextOffset; | |
| let field; | |
| for (let i = 0; i < fieldOffsets.length; i++) { | |
| fieldId = fieldOffsets[i][0]; | |
| offset = fieldOffsets[i][1]; | |
| nextOffset = i + 1 < fieldOffsets.length ? fieldOffsets[i + 1][1] : this._schemaOffset; | |
| field = BinaryObjectField._fromBuffer( | |
| communicator,this._buffer, this._startPos + offset, nextOffset - offset, fieldId); | |
| this._fields.set(field.id, field); | |
| } | |
| } | |
| this._buffer.position = this._startPos + this._length; | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| async _readHeader(communicator) { | |
| // type code | |
| this._buffer.readByte(); | |
| // version | |
| const version = this._buffer.readByte(); | |
| if (version !== VERSION) { | |
| throw IgniteClientError.internalError(); | |
| } | |
| // flags | |
| const flags = this._buffer.readShort(); | |
| // type id | |
| const typeId = this._buffer.readInteger(); | |
| // hash code | |
| this._hashCode = this._buffer.readInteger(); | |
| // length | |
| this._length = this._buffer.readInteger(); | |
| // schema id | |
| const schemaId = this._buffer.readInteger(); | |
| // schema offset | |
| this._schemaOffset = this._buffer.readInteger(); | |
| this._hasSchema = BinaryObject._isFlagSet(flags, FLAG_HAS_SCHEMA); | |
| this._compactFooter = BinaryObject._isFlagSet(flags, FLAG_COMPACT_FOOTER); | |
| this._hasRawData = BinaryObject._isFlagSet(flags, FLAG_HAS_RAW_DATA); | |
| this._offsetType = BinaryObject._isFlagSet(flags, FLAG_OFFSET_ONE_BYTE) ? | |
| BinaryUtils.TYPE_CODE.BYTE : | |
| BinaryObject._isFlagSet(flags, FLAG_OFFSET_TWO_BYTES) ? | |
| BinaryUtils.TYPE_CODE.SHORT : | |
| BinaryUtils.TYPE_CODE.INTEGER; | |
| this._typeBuilder = await BinaryTypeBuilder.fromTypeId(communicator, typeId, this._compactFooter ? schemaId : null); | |
| } | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| export class BinaryObjectField { | |
| private _name: string; | |
| private _id: number; | |
| private _value: object; | |
| private _type: PRIMITIVE_TYPE | CompositeType; | |
| private _typeCode: number; | |
| private _communicator: BinaryCommunicator; | |
| private _buffer: MessageBuffer; | |
| private _offset: number; | |
| private _length: number; | |
| constructor(name: string, value = undefined, type = null) { | |
| this._name = name; | |
| this._id = BinaryField._calculateId(name); | |
| this._value = value; | |
| this._type = type; | |
| if (!type && value !== undefined && value !== null) { | |
| this._type = BinaryUtils.calcObjectType(value); | |
| } | |
| this._typeCode = null; | |
| if (this._type) { | |
| this._typeCode = BinaryUtils.getTypeCode(this._type); | |
| } | |
| } | |
| get id() { | |
| return this._id; | |
| } | |
| get typeCode() { | |
| return this._typeCode; | |
| } | |
| async getValue(type = null) { | |
| if (this._value === undefined || this._buffer && this._type !== type) { | |
| this._buffer.position = this._offset; | |
| this._value = await this._communicator.readObject(this._buffer, type); | |
| this._type = type; | |
| } | |
| return this._value; | |
| } | |
| getOffsetType(headerStartPos) { | |
| let offset = this._offset - headerStartPos; | |
| if (offset < 0x100) { | |
| return BinaryUtils.TYPE_CODE.BYTE; | |
| } | |
| else if (offset < 0x10000) { | |
| return BinaryUtils.TYPE_CODE.SHORT; | |
| } | |
| return BinaryUtils.TYPE_CODE.INTEGER; | |
| } | |
| static _fromBuffer(communicator: BinaryCommunicator, buffer: MessageBuffer, offset: number, length: number, id: number) { | |
| const result = new BinaryObjectField(null); | |
| result._id = id; | |
| result._communicator = communicator; | |
| result._buffer = buffer; | |
| result._offset = offset; | |
| result._length = length; | |
| return result; | |
| } | |
| async _writeValue(communicator, buffer, expectedTypeCode) { | |
| const offset = buffer.position; | |
| if (this._buffer && this._communicator === communicator) { | |
| buffer.writeBuffer(this._buffer.buffer, this._offset, this._offset + this._length); | |
| } | |
| else { | |
| if (this._value === undefined) { | |
| await this.getValue(); | |
| } | |
| BinaryUtils.checkCompatibility(this._value, expectedTypeCode); | |
| await communicator.writeObject(buffer, this._value, this._type); | |
| } | |
| this._communicator = communicator; | |
| this._buffer = buffer; | |
| this._length = buffer.position - offset; | |
| this._offset = offset; | |
| } | |
| _writeOffset(buffer, headerStartPos, offsetType) { | |
| buffer.writeNumber(this._offset - headerStartPos, offsetType, false); | |
| } | |
| } |