blob: 92a32949f58fb17b708773b6f95c49880744a6a9 [file] [log] [blame]
<?php
/*
* 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.
*/
namespace Apache\Ignite\Data;
use Apache\Ignite\Type\ObjectType;
use Apache\Ignite\Type\ComplexObjectType;
use Apache\Ignite\Exception\ClientException;
use Apache\Ignite\Internal\Binary\BinaryCommunicator;
use Apache\Ignite\Internal\Binary\BinaryTypeBuilder;
use Apache\Ignite\Internal\Binary\BinaryObjectField;
use Apache\Ignite\Internal\Binary\MessageBuffer;
use Apache\Ignite\Internal\Binary\BinaryUtils;
use Apache\Ignite\Internal\Binary\BinaryField;
use Apache\Ignite\Internal\Utils\ArgumentChecker;
use Apache\Ignite\Internal\Utils\Logger;
/**
* Class representing a complex Ignite object in the binary form.
*
* It corresponds to ObjectType::COMPLEX_OBJECT,
* 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 PHP object.
* - created using the public constructor. Fields may be added to such an instance using setField() method.
* - created from a PHP object using static fromObject() method.
*/
class BinaryObject
{
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;
private $typeName;
private $fields;
private $typeBuilder;
private $modified;
private $buffer;
private $schemaOffset;
private $hasSchema;
private $compactFooter;
private $hasRawData;
private $offsetType;
private $startPos;
private $length;
/**
* 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.
*
* @throws ClientException if error.
*/
public function __construct(string $typeName)
{
ArgumentChecker::notEmpty($typeName, 'typeName');
$this->typeName = $typeName;
$this->fields = [];
$this->typeBuilder = BinaryTypeBuilder::fromTypeName($typeName);
$this->modified = false;
$this->buffer = null;
$this->schemaOffset = null;
$this->hasSchema = false;
$this->compactFooter = false;
$this->hasRawData = false;
}
/**
* Creates an instance of the BinaryObject from the specified instance of PHP class.
*
* All public properties of the PHP object with their values are added as fields
* 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 PHP class which instance is specified.
*
* @param object $object instance of PHP class 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 ClientException if error.
*/
public static function fromObject(object $object, ComplexObjectType $complexObjectType = null): BinaryObject
{
$typeBuilder = BinaryTypeBuilder::fromObject($object, $complexObjectType);
$result = new BinaryObject($typeBuilder->getTypeName());
$result->typeBuilder = $typeBuilder;
try {
$class = new \ReflectionClass($object);
foreach ($typeBuilder->getFields() as $field) {
$fieldName = $field->getName();
if ($class->hasProperty($fieldName)) {
$result->setField(
$fieldName,
$class->getProperty($fieldName)->getValue($object),
$complexObjectType ? $complexObjectType->getFieldType($fieldName) : null);
} else {
BinaryUtils::serializationError(true, sprintf('field "%s" does not exist', $fieldName));
}
}
} catch (\ReflectionException $e) {
BinaryUtils::serializationError(true, sprintf('class "%s" does not exist', get_class($object)));
}
return $result;
}
/**
* Sets new value of the specified field.
* Adds the specified field, if it did not exist before.
*
* Optionally, specifies an Ignite type of the field.
* If the type is not specified then the Ignite client
* tries to make automatic mapping between PHP types and Ignite object types -
* according to the mapping table defined in the description of the ObjectType class.
*
* @param string $fieldName name of the field.
* @param mixed $fieldValue new value of the field.
* @param int|ObjectType|null $fieldType Ignite type of the field:
* - either a type code of primitive (simple) type (@ref PrimitiveTypeCodes)
* - 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 ClientException if error.
*/
public function setField(string $fieldName, $fieldValue, $fieldType = null): BinaryObject
{
ArgumentChecker::notEmpty($fieldName, 'fieldName');
BinaryUtils::checkObjectType($fieldType, 'fieldType');
$this->modified = true;
$field = new BinaryObjectField($fieldName, $fieldValue, $fieldType);
$this->fields[$field->getId()] = $field;
$this->typeBuilder->setField($fieldName, $field->getTypeCode());
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 ClientException if error.
*/
public function removeField(string $fieldName): BinaryObject
{
ArgumentChecker::notEmpty($fieldName, 'fieldName');
$this->modified = true;
$fieldId = BinaryField::calculateId($fieldName);
if (array_key_exists($fieldId, $this->fields)) {
unset($this->fields[$fieldId]);
}
$this->typeBuilder->removeField($fieldName);
return $this;
}
/**
* Checks if the specified field exists in this BinaryObject instance.
*
* @param string $fieldName name of the field.
*
* @return bool true if exists, false otherwise.
*
* @throws ClientException if error.
*/
public function hasField(string $fieldName): bool
{
ArgumentChecker::notEmpty($fieldName, 'fieldName');
$fieldId = BinaryField::calculateId($fieldName);
return array_key_exists($fieldId, $this->fields);
}
/**
* Returns a value of the specified field.
*
* Optionally, specifies Ignite type of the field.
* If the type is not specified then the Ignite client
* tries to make automatic mapping between PHP types and Ignite object types -
* according to the mapping table defined in the description of the ObjectType class.
*
* If field with the specified name doesn't exist, throws Exception::ClientException.
* Use hasField() method to ensure the field exists.
*
* @param string $fieldName name of the field.
* @param int|ObjectType|null $fieldType Ignite type of the field:
* - either a type code of primitive (simple) type (@ref PrimitiveTypeCodes)
* - or an instance of class representing non-primitive (composite) type
* - or null (or not specified) that means the type is not specified
*
* @return mixed value of the field.
*
* @throws ClientException if error.
*/
public function getField(string $fieldName, $fieldType = null)
{
ArgumentChecker::notEmpty($fieldName, 'fieldName');
BinaryUtils::checkObjectType($fieldType, 'fieldType');
$fieldId = BinaryField::calculateId($fieldName);
if (array_key_exists($fieldId, $this->fields)) {
return $this->fields[$fieldId]->getValue($fieldType);
}
throw new ClientException(sprintf('Field %s does not exist', $fieldName));
}
/**
* Deserializes this BinaryObject instance into PHP object which corresponds to the specified complex object type.
*
* @param ComplexObjectType $complexObjectType instance of class representing complex object type.
*
* @return object instance of the PHP object which corresponds to the specified complex object type.
*
* @throws ClientException if error.
*/
public function toObject(ComplexObjectType $complexObjectType): object
{
$className = $complexObjectType->getPhpClassName();
if (!$className) {
$className = $this->getTypeName();
}
if (!class_exists($className)) {
BinaryUtils::serializationError(false, sprintf('class "%s" does not exist', $className));
}
$result = new $className;
foreach ($this->fields as $field) {
$binaryField = $this->typeBuilder->getField($field->getId());
if (!$binaryField) {
BinaryUtils::serializationError(
false, sprintf('field with id "%s" can not be deserialized', $field->getId()));
}
$fieldName = $binaryField->getName();
$result->$fieldName = $field->getValue($complexObjectType->getFieldType($fieldName));
}
return $result;
}
/**
* Returns type name of this BinaryObject instance.
*
* @return string type name.
*/
public function getTypeName(): string
{
return $this->typeBuilder->getTypeName();
}
/**
* Returns names of all fields in this BinaryObject instance.
*
* @return array names of all fields.
*
* @throws ClientException if error.
*/
public function getFieldNames(): array
{
return array_map(
function (int $fieldId): string
{
$field = $this->typeBuilder->getField($fieldId);
if ($field) {
return $field->getName();
} else {
BinaryUtils::internalError(
sprintf('Field "%s" is absent in binary type fields', $fieldId));
}
return null;
},
$this->typeBuilder->getSchema()->getFieldIds());
}
private static function isFlagSet(int $flags, int $flag): bool
{
return ($flags & $flag) === $flag;
}
// This is not the public API method, is not intended for usage by an application.
public static function fromBuffer(BinaryCommunicator $communicator, MessageBuffer $buffer): BinaryObject
{
$result = new BinaryObject(' ');
$result->buffer = $buffer;
$result->startPos = $buffer->getPosition();
$result->read($communicator);
return $result;
}
// This is not the public API method, is not intended for usage by an application.
public function write(BinaryCommunicator $communicator, MessageBuffer $buffer): void
{
if ($this->buffer && !$this->modified) {
$buffer->writeBuffer($this->buffer, $this->startPos, $this->length);
} else {
$this->typeBuilder->finalize($communicator);
$this->startPos = $buffer->getPosition();
$buffer->setPosition($this->startPos + BinaryObject::HEADER_LENGTH);
$this->hasSchema = count($this->fields) > 0;
if ($this->hasSchema) {
// write fields
$field = null;
foreach ($this->fields as $field) {
$field->writeValue($communicator, $buffer, ($this->typeBuilder->getField($field->getId()))->getTypeCode());
}
$this->schemaOffset = $buffer->getPosition() - $this->startPos;
$this->offsetType = $field->getOffsetType($this->startPos);
// write schema
foreach ($this->fields as $field) {
$field->writeOffset($buffer, $this->startPos, $this->offsetType);
}
} else {
$this->schemaOffset = 0;
}
$this->length = $buffer->getPosition() - $this->startPos;
$this->buffer = $buffer;
// write header
$this->writeHeader();
$this->buffer->setPosition($this->startPos + $this->length);
$this->modified = false;
}
if (Logger::isDebug()) {
Logger::logDebug('BinaryObject::write');
Logger::logBuffer($this->buffer, $this->startPos, $this->length);
}
}
private function writeHeader(): void
{
$this->buffer->setPosition($this->startPos);
// type code
$this->buffer->writeByte(ObjectType::COMPLEX_OBJECT);
// version
$this->buffer->writeByte(BinaryObject::VERSION);
// flags
$flags = BinaryObject::FLAG_USER_TYPE;
if ($this->hasSchema) {
$flags = $flags | BinaryObject::FLAG_COMPACT_FOOTER | BinaryObject::FLAG_HAS_SCHEMA;
}
if ($this->offsetType === ObjectType::BYTE) {
$flags = $flags | BinaryObject::FLAG_OFFSET_ONE_BYTE;
} elseif ($this->offsetType === ObjectType::SHORT) {
$flags = $flags | BinaryObject::FLAG_OFFSET_TWO_BYTES;
}
$this->buffer->writeShort($flags);
// type id
$this->buffer->writeInteger($this->typeBuilder->getTypeId());
// hash code
$this->buffer->writeInteger(BinaryUtils::contentHashCode(
$this->buffer, $this->startPos + BinaryObject::HEADER_LENGTH, $this->schemaOffset - 1));
// length
$this->buffer->writeInteger($this->length);
// schema id
$this->buffer->writeInteger($this->hasSchema ? $this->typeBuilder->getSchemaId() : 0);
// schema offset
$this->buffer->writeInteger($this->schemaOffset);
}
private function read(BinaryCommunicator $communicator): void
{
$this->readHeader($communicator);
if ($this->hasSchema) {
$this->buffer->setPosition($this->startPos + $this->schemaOffset);
$fieldOffsets = [];
$fieldIds = $this->typeBuilder->getSchema()->getFieldIds();
$index = 0;
$schemaEndOffset = $this->startPos + $this->length;
if ($this->hasRawData) {
$schemaEndOffset -= TypeInfo::getTypeInfo(ObjectType::INTEGER)->getSize();
}
while ($this->buffer->getPosition() < $schemaEndOffset) {
if (!$this->compactFooter) {
$fieldId = $this->buffer->readInteger();
$this->typeBuilder->getSchema()->addField($fieldId);
} else {
if ($index >= count($fieldIds)) {
BinaryUtils::serializationError(
false, 'wrong number of fields in schema');
}
$fieldId = $fieldIds[$index];
$index++;
}
array_push($fieldOffsets, [$fieldId, $this->buffer->readNumber($this->offsetType, false)]);
}
usort($fieldOffsets,
function (array $val1, array $val2): int
{
return $val1[1] - $val2[1];
});
for ($i = 0; $i < count($fieldOffsets); $i++) {
$fieldId = $fieldOffsets[$i][0];
$offset = $fieldOffsets[$i][1];
$nextOffset = $i + 1 < count($fieldOffsets) ? $fieldOffsets[$i + 1][1] : $this->schemaOffset;
$field = BinaryObjectField::fromBuffer(
$communicator, $this->buffer, $this->startPos + $offset, $nextOffset - $offset, $fieldId);
$this->fields[$field->getId()] = $field;
}
}
$this->buffer->setPosition($this->startPos + $this->length);
}
private function readHeader(BinaryCommunicator $communicator): void
{
// type code
$this->buffer->readByte();
// version
$version = $this->buffer->readByte();
if ($version !== BinaryObject::VERSION) {
BinaryUtils::internalError();
}
// flags
$flags = $this->buffer->readShort();
// type id
$typeId = $this->buffer->readInteger();
// hash code
$this->buffer->readInteger();
// length
$this->length = $this->buffer->readInteger();
// schema id
$schemaId = $this->buffer->readInteger();
// schema offset
$this->schemaOffset = $this->buffer->readInteger();
$this->hasSchema = BinaryObject::isFlagSet($flags, BinaryObject::FLAG_HAS_SCHEMA);
$this->compactFooter = BinaryObject::isFlagSet($flags, BinaryObject::FLAG_COMPACT_FOOTER);
$this->hasRawData = BinaryObject::isFlagSet($flags, BinaryObject::FLAG_HAS_RAW_DATA);
$this->offsetType = BinaryObject::isFlagSet($flags, BinaryObject::FLAG_OFFSET_ONE_BYTE) ?
ObjectType::BYTE :
(BinaryObject::isFlagSet($flags, BinaryObject::FLAG_OFFSET_TWO_BYTES) ?
ObjectType::SHORT :
ObjectType::INTEGER);
$this->typeBuilder = BinaryTypeBuilder::fromTypeId($communicator, $typeId, $this->compactFooter ? $schemaId : null);
}
}