tao-test/app/generis/common/persistence/class.KeyValuePersistence.php

500 lines
14 KiB
PHP

<?php
/**
* This program 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; under version 2
* of the License (non-upgradable).
*
* This program 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.
*
* Copyright (c) 2013-2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
* @author Lionel Lecaque <lionel@taotesting.com>
* @author Camille Moyon <camille@taotesting.com>
* @license GPLv2
* @package generis
*
*/
class common_persistence_KeyValuePersistence extends common_persistence_Persistence
{
/**
* Ability to set the key only if it does not already exist
*/
const FEATURE_NX = 'nx';
const MAX_VALUE_SIZE = 'max_value_size';
const MAP_IDENTIFIER = 'map_identifier';
const START_MAP_DELIMITER = 'start_map_delimiter';
const END_MAP_DELIMITER = 'end_map_delimiter';
const MAPPED_KEY_SEPARATOR = '###';
const LEVEL_SEPARATOR = '-';
const DEFAULT_MAP_IDENTIFIER = '<<<<mapped>>>>';
const DEFAULT_START_MAP_DELIMITER = '<<<<mappedKey>>>>';
const DEFAULT_END_MAP_DELIMITER = '<<<</mappedKey>>>>';
/**
* @var int The maximum size allowed for the value
*/
protected $size = false;
/**
* Set a $key with a $value
* If $value is too large, it is split into multiple $mappedKey.
* These new keys are serialized and stored into actual $key
*
* @param string $key
* @param string $value
* @param string $ttl
* @param bool $nx
* @return bool
* @throws common_Exception If size is misconfigured
*/
public function set($key, $value, $ttl = null, $nx = false)
{
if ($this->hasMaxSize()) {
if ($this->isLarge($value)) {
$value = $this->setLargeValue($key, $value, 0, true, true, $ttl, $nx);
}
}
return $this->getDriver()->set($key, $value, $ttl, $nx);
}
/**
* Get $key from driver. If $key is split, all mapped values are retrieved and join to restore original value
*
* @param string $key
* @return bool|int|null|string
*/
public function get($key)
{
$value = $this->getDriver()->get($key);
if ($this->hasMaxSize()) {
if ($this->isSplit($value)) {
$value = $this->join($key, $value);
}
}
return $value;
}
/**
* Check if a key exists
* Return false if $key is a mappedKey
*
* @param $key
* @return bool
*/
public function exists($key)
{
if ($this->isMappedKey($key)) {
return false;
} else {
return $this->getDriver()->exists($key);
}
}
/**
* Delete a key. If key is split, all associated mapped key are deleted too
*
* @param $key
* @return bool
*/
public function del($key)
{
if ($this->isMappedKey($key)) {
return false;
} else {
$success = true;
if ($this->hasMaxSize()) {
$success = $this->deleteMappedKey($key);
}
return $success && $this->getDriver()->del($key);
}
}
/**
* Increment $key, only for numeric
* Mapped key will be ignored
*
* @param $key
* @return bool|int
*/
public function incr($key)
{
if ($this->isMappedKey($key)) {
return false;
}
return $this->getDriver()->incr($key);
}
/**
* Decrement $key, only for numeric
* Mapped key will be ignored
*
* @param $key
* @return bool|int
*/
public function decr($key)
{
if ($this->isMappedKey($key)) {
return false;
}
return $this->getDriver()->decr($key);
}
/**
* Delete a key and if the value is a map, delete all mapped key recursively
*
* @param $key
* @param null $value
* @param int $level
* @return bool
*/
protected function deleteMappedKey($key, $value = null, $level = 0)
{
if (is_null($value)) {
$value = $this->getDriver()->get($key);
}
if ($level > 0) {
$key = $key . self::LEVEL_SEPARATOR . $level;
}
$success = true;
if ($this->isSplit($value)) {
$valueParts = [];
foreach ($this->unSerializeMap($value) as $mappedKey) {
$mappedKey = $this->transformReferenceToMappedKey($mappedKey);
$valueParts[$this->getMappedKeyIndex($mappedKey, $key)] = $this->getDriver()->get($mappedKey);
$success = $success && $this->getDriver()->del($mappedKey);
}
uksort($valueParts, 'strnatcmp');
$value = implode('', $valueParts);
if ($this->isSplit($value)) {
$success = $success && $this->deleteMappedKey($key, $value, $level + 1);
}
}
return $success;
}
/**
* Purge the Driver if it implements common_persistence_Purgable
* Otherwise throws common_exception_NotImplemented
*
* @return mixed
* @throws common_exception_NotImplemented
*/
public function purge()
{
if ($this->getDriver() instanceof common_persistence_Purgable) {
return $this->getDriver()->purge();
} else {
throw new common_exception_NotImplemented("purge not implemented ");
}
}
/**
* Set a large value recursively.
* Create a map of value (split by size range) and store the serialize map as current value
*
* @param $key
* @param $value
* @param int $level
* @param bool $flush
* @param bool $toTransform
* @param null $ttl
* @param bool $nx
* @return mixed
* @throws common_Exception
*/
protected function setLargeValue($key, $value, $level = 0, $flush = true, $toTransform = true, $ttl = null, $nx = false)
{
if (!$this->isLarge($value)) {
if ($flush) {
$this->set($key, $value, $ttl, $nx);
}
return $value;
}
if ($nx) {
throw new common_exception_NotImplemented("NX not implemented for large values");
}
if ($level > 0) {
$key = $key . self::LEVEL_SEPARATOR . $level;
}
$map = $this->createMap($key, $value);
foreach ($map as $mappedKey => $valuePart) {
if ($toTransform) {
$transformedKey = $this->transformReferenceToMappedKey($mappedKey);
} else {
$transformedKey = $mappedKey;
}
if (!is_null($ttl)) {
$this->set($transformedKey, $valuePart, $ttl);
} else {
$this->set($transformedKey, $valuePart);
}
}
return $this->setLargeValue($key, $this->serializeMap($map), $level + 1, $flush, $toTransform, $ttl);
}
/**
* Check if the given $value is larger than $this max size
*
* @param $value
* @return bool
* @throws common_Exception If size is misconfigured
*/
protected function isLarge($value)
{
$size = $this->getSize();
if (!$size) {
return false;
}
return strlen($value) > $size;
}
/**
* Cut a string into an array with $size option
*
* @param $value
* @return array
* @throws common_Exception If size is misconfigured
*/
protected function split($value)
{
return str_split($value, $this->getSize());
}
/**
* Join different values referenced into a map recursively
*
* @param $key
* @param $value
* @param int $level
* @return string
*/
protected function join($key, $value, $level = 0)
{
if ($level > 0) {
$key = $key . self::LEVEL_SEPARATOR . $level;
}
$valueParts = [];
foreach ($this->unSerializeMap($value) as $mappedKey) {
$mappedKey = $this->transformReferenceToMappedKey($mappedKey);
$valueParts[$this->getMappedKeyIndex($mappedKey, $key)] = $this->getDriver()->get($mappedKey);
}
uksort($valueParts, 'strnatcmp');
$value = implode('', $valueParts);
if ($this->isSplit($value)) {
$value = $this->join($key, $value, $level + 1);
}
return $value;
}
/**
* Split a large value to an array with value size lesser than required max size
* Construct the array with index of value
*
* @param $key
* @param $value
* @return array
* @throws common_Exception If size is misconfigured
*/
protected function createMap($key, $value)
{
$splitValue = $this->split($value);
$map = [];
foreach ($splitValue as $index => $part) {
$map[$key . self::MAPPED_KEY_SEPARATOR . $index] = $part;
}
return $map;
}
/**
* Transform a map reference to an identifiable $key
*
* @param $key
* @return string
*/
protected function transformReferenceToMappedKey($key)
{
return $this->getStartMapDelimiter() . $key . $this->getEndMapDelimiter();
}
/**
* Check if current $key is part of a map
*
* @param $key
* @return bool
*/
protected function isMappedKey($key)
{
return substr($key, 0, strlen($this->getStartMapDelimiter())) == $this->getStartMapDelimiter()
&& substr($key, -strlen($this->getEndMapDelimiter())) == $this->getEndMapDelimiter();
}
/**
* Get the mapped key index of a mappedKey
*
* @param $mappedKey
* @param $key
* @return bool|string
*/
protected function getMappedKeyIndex($mappedKey, $key)
{
$startSize = strlen($this->getStartMapDelimiter()) - 1;
$key = substr($key, $startSize, strrpos($key, $this->getEndMapDelimiter()) - $startSize);
return substr($mappedKey, strlen($key . self::MAPPED_KEY_SEPARATOR));
}
/**
* Serialize a map to set it as a value
*
* @param array $map
* @return string
*/
protected function serializeMap(array $map)
{
return $this->getMapIdentifier() . json_encode(array_keys($map));
}
/**
* Unserialize a map that contains references to mapped keys
*
* @param $map
* @return mixed
*/
protected function unSerializeMap($map)
{
return json_decode(substr_replace($map, '', 0, strlen($this->getMapIdentifier())), true);
}
/**
* Check if the value was split into couple of values.
* Identifiable by the map identifier at beginning
*
* @param $value
* @return bool
*/
protected function isSplit($value)
{
if (!is_string($value)) {
return false;
}
return strpos($value, $this->getMapIdentifier()) === 0;
}
/**
* Get the current maximum allowed size for a value
*
* @return int
* @throws common_Exception If size is set
*/
protected function getSize()
{
if (! $this->size) {
$size = $this->getParam(self::MAX_VALUE_SIZE);
if ($size !== false) {
if (!is_int($size)) {
throw new common_Exception('Persistence max value size has to be an integer');
}
$this->size = $size - strlen($this->getMapIdentifier());
}
}
return $this->size;
}
/**
* Check if the current persistence has a max size parameter
*
* @return boolean
*/
protected function hasMaxSize()
{
return $this->getParam(self::MAX_VALUE_SIZE) !== false;
}
/**
* Get the identifier to identify a value as split. Should be no used into beginning of none mapped key
*
* @return string
*/
protected function getMapIdentifier()
{
return $this->getParam(self::MAP_IDENTIFIER)
?: self::DEFAULT_MAP_IDENTIFIER;
}
/**
* Get the start-map delimiter from config, otherwise fallback to default value
*
* @return string
*/
protected function getStartMapDelimiter()
{
return $this->getParam(self::START_MAP_DELIMITER)
?: self::DEFAULT_START_MAP_DELIMITER;
}
/**
* Get the end-map delimiter from config, otherwise fallback to default value
*
* @return string
*/
protected function getEndMapDelimiter()
{
return $this->getParam(self::END_MAP_DELIMITER)
?: self::DEFAULT_END_MAP_DELIMITER;
}
/**
* Get the requested param from current parameters, otherwise throws exception
*
* @param $param
* @return mixed
*/
protected function getParam($param)
{
$params = $this->getParams();
if (! isset($params[$param])) {
return false;
}
return $params[$param];
}
/**
* Test wheever or not a feature is supported
* @param string $feature
* @throws common_exception_Error if feature is unkown
* @return boolean
*/
public function supportsFeature($feature)
{
switch ($feature) {
case self::FEATURE_NX:
return ($this->getDriver() instanceof common_persistence_KeyValue_Nx);
default:
throw new common_exception_Error('Unknown feature ' . $feature);
}
return false;
}
}