diff options
Diffstat (limited to '')
-rw-r--r-- | library/Director/Data/Db/DbConnection.php | 51 | ||||
-rw-r--r-- | library/Director/Data/Db/DbDataFormatter.php | 26 | ||||
-rw-r--r-- | library/Director/Data/Db/DbObject.php | 1487 | ||||
-rw-r--r-- | library/Director/Data/Db/DbObjectStore.php | 169 | ||||
-rw-r--r-- | library/Director/Data/Db/DbObjectTypeRegistry.php | 75 | ||||
-rw-r--r-- | library/Director/Data/Db/DbObjectWithSettings.php | 168 | ||||
-rw-r--r-- | library/Director/Data/Db/IcingaObjectFilterRenderer.php | 133 | ||||
-rw-r--r-- | library/Director/Data/Db/IcingaObjectQuery.php | 255 | ||||
-rw-r--r-- | library/Director/Data/Db/ServiceSetQueryBuilder.php | 158 |
9 files changed, 2522 insertions, 0 deletions
diff --git a/library/Director/Data/Db/DbConnection.php b/library/Director/Data/Db/DbConnection.php new file mode 100644 index 0000000..146b0e8 --- /dev/null +++ b/library/Director/Data/Db/DbConnection.php @@ -0,0 +1,51 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Data\Db\DbConnection as IcingaDbConnection; +use Icinga\Module\Director\Db\DbUtil; +use RuntimeException; +use Zend_Db_Expr; + +class DbConnection extends IcingaDbConnection +{ + public function isMysql() + { + return $this->getDbType() === 'mysql'; + } + + public function isPgsql() + { + return $this->getDbType() === 'pgsql'; + } + + /** + * @deprecated + * @param ?string $binary + * @return Zend_Db_Expr|Zend_Db_Expr[]|null + */ + public function quoteBinary($binary) + { + return DbUtil::quoteBinaryLegacy($binary, $this->getDbAdapter()); + } + + public function binaryDbResult($value) + { + if (is_resource($value)) { + return stream_get_contents($value); + } + + return $value; + } + + public function hasPgExtension($name) + { + $db = $this->db(); + $query = $db->select()->from( + array('e' => 'pg_extension'), + array('cnt' => 'COUNT(*)') + )->where('extname = ?', $name); + + return (int) $db->fetchOne($query) === 1; + } +} diff --git a/library/Director/Data/Db/DbDataFormatter.php b/library/Director/Data/Db/DbDataFormatter.php new file mode 100644 index 0000000..d6e4eeb --- /dev/null +++ b/library/Director/Data/Db/DbDataFormatter.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use InvalidArgumentException; + +class DbDataFormatter +{ + public static function normalizeBoolean($value) + { + if ($value === 'y' || $value === '1' || $value === true || $value === 1) { + return 'y'; + } + if ($value === 'n' || $value === '0' || $value === false || $value === 0) { + return 'n'; + } + if ($value === '' || $value === null) { + return null; + } + + throw new InvalidArgumentException(sprintf( + 'Got invalid boolean: %s', + var_export($value, 1) + )); + } +} diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php new file mode 100644 index 0000000..6ecae8b --- /dev/null +++ b/library/Director/Data/Db/DbObject.php @@ -0,0 +1,1487 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Data\InvalidDataException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\UuidLookup; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use InvalidArgumentException; +use LogicException; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; +use RuntimeException; +use Zend_Db_Adapter_Abstract; +use Zend_Db_Exception; + +/** + * Base class for ... + */ +abstract class DbObject +{ + /** @var DbConnection $connection */ + protected $connection; + + /** @var string Table name. MUST be set when extending this class */ + protected $table; + + /** @var Zend_Db_Adapter_Abstract */ + protected $db; + + /** + * Default columns. MUST be set when extending this class. Each table + * column MUST be defined with a default value. Default value may be null. + * + * @var array + */ + protected $defaultProperties; + + /** + * Properties as loaded from db + */ + protected $loadedProperties; + + /** + * Whether at least one property has been modified + */ + protected $hasBeenModified = false; + + /** + * Whether this object has been loaded from db + */ + protected $loadedFromDb = false; + + /** + * Object properties + */ + protected $properties = array(); + + /** + * Property names that have been modified since object creation + */ + protected $modifiedProperties = array(); + + /** + * Unique key name, could be primary + */ + protected $keyName; + + /** + * Set this to an eventual autoincrementing column. May equal $keyName + */ + protected $autoincKeyName; + + /** @var string optional uuid column */ + protected $uuidColumn; + + /** @var bool forbid updates to autoinc values */ + protected $protectAutoinc = true; + + protected $binaryProperties = []; + + /** + * Filled with object instances when prefetchAll is used + */ + protected static $prefetched = array(); + + /** + * object_name => id map for prefetched objects + */ + protected static $prefetchedNames = array(); + + protected static $prefetchStats = array(); + + /** @var ?DbObjectStore */ + protected static $dbObjectStore; + + /** + * Constructor is not accessible and should not be overridden + */ + protected function __construct() + { + if ($this->table === null + || $this->keyName === null + || $this->defaultProperties === null + ) { + throw new LogicException("Someone extending this class didn't RTFM"); + } + + $this->properties = $this->defaultProperties; + $this->beforeInit(); + } + + public function getTableName() + { + return $this->table; + } + + /************************************************************************\ + * When extending this class one might want to override any of the * + * following hooks. Try to use them whenever possible, especially * + * instead of overriding other essential methods like store(). * + \************************************************************************/ + + /** + * One can override this to allow for cross checks and more before storing + * the object. Please note that the method is public and allows to check + * object consistence at any time. + * + * @return boolean Whether this object is valid + */ + public function validate() + { + return true; + } + + /** + * This is going to be executed before any initialization method takes * + * (load from DB, populate from Array...) takes place + * + * @return void + */ + protected function beforeInit() + { + } + + /** + * Will be executed every time an object has successfully been loaded from + * Database + * + * @return void + */ + protected function onLoadFromDb() + { + } + + /** + * Will be executed before an Object is going to be stored. In case you + * want to prevent the store() operation from taking place, please throw + * an Exception. + * + * @return void + */ + protected function beforeStore() + { + } + + /** + * Wird ausgeführt, nachdem ein Objekt erfolgreich gespeichert worden ist + * + * @return void + */ + protected function onStore() + { + } + + /** + * Wird ausgeführt, nachdem ein Objekt erfolgreich der Datenbank hinzu- + * gefügt worden ist + * + * @return void + */ + protected function onInsert() + { + } + + /** + * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich der Datenbank + * geändert worden ist + * + * @return void + */ + protected function onUpdate() + { + } + + /** + * Wird ausgeführt, bevor ein Objekt gelöscht wird. Die Operation wird + * aber auf jeden Fall durchgeführt, außer man wirft eine Exception + * + * @return void + */ + protected function beforeDelete() + { + } + + /** + * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich aud der + * Datenbank gelöscht worden ist + * + * @return void + */ + protected function onDelete() + { + } + + /** + * Set database connection + * + * @param DbConnection $connection Database connection + * + * @return self + */ + public function setConnection(DbConnection $connection) + { + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + + return $this; + } + + public static function setDbObjectStore(DbObjectStore $store) + { + self::$dbObjectStore = $store; + } + + /** + * Getter + * + * @param string $property Property + * + * @return mixed + */ + public function get($property) + { + $func = 'get' . ucfirst($property); + if (substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + // TODO: id check avoids collision with getId. Rethink this. + if ($property !== 'id' && method_exists($this, $func)) { + return $this->$func(); + } + + $this->assertPropertyExists($property); + return $this->properties[$property]; + } + + public function getProperty($key) + { + $this->assertPropertyExists($key); + return $this->properties[$key]; + } + + protected function assertPropertyExists($key) + { + if (! array_key_exists($key, $this->properties)) { + throw new InvalidArgumentException(sprintf( + 'Trying to get invalid property "%s"', + $key + )); + } + + return $this; + } + + public function hasProperty($key) + { + if (array_key_exists($key, $this->properties)) { + return true; + } elseif ($key === 'id') { + // There is getId, would give false positive + return false; + } + + return $this->hasGetterForProperty($key); + } + + protected function hasGetterForProperty($key) + { + $func = 'get' . ucfirst($key); + if (\substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + + return \method_exists($this, $func); + } + + protected function hasSetterForProperty($key) + { + $func = 'set' . ucfirst($key); + if (\substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + + return \method_exists($this, $func); + } + + /** + * Generic setter + * + * @param string $key + * @param mixed $value + * + * @return self + */ + public function set($key, $value) + { + $key = (string) $key; + if ($value === '') { + $value = null; + } + + if (is_resource($value)) { + $value = stream_get_contents($value); + } + $func = 'validate' . ucfirst($key); + if (method_exists($this, $func) && $this->$func($value) !== true) { + throw new InvalidArgumentException(sprintf( + 'Got invalid value "%s" for "%s"', + $value, + $key + )); + } + $func = 'munge' . ucfirst($key); + if (method_exists($this, $func)) { + $value = $this->$func($value); + } + + $func = 'set' . ucfirst($key); + if (substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + + if (method_exists($this, $func)) { + return $this->$func($value); + } + + if (! $this->hasProperty($key)) { + throw new InvalidArgumentException(sprintf( + 'Trying to set invalid key "%s"', + $key + )); + } + + if ((is_numeric($value) || is_string($value)) + && (string) $value === (string) $this->get($key) + ) { + return $this; + } + + if ($key === $this->getAutoincKeyName() && $this->hasBeenLoadedFromDb()) { + throw new InvalidArgumentException('Changing autoincremental key is not allowed'); + } + + return $this->reallySet($key, $value); + } + + protected function reallySet($key, $value) + { + if ($value === $this->properties[$key]) { + return $this; + } + if ($key === 'id' || substr($key, -3) === '_id') { + if ((int) $value === (int) $this->properties[$key]) { + return $this; + } + } + + if ($this->hasBeenLoadedFromDb()) { + if ($value === $this->loadedProperties[$key]) { + unset($this->modifiedProperties[$key]); + if (empty($this->modifiedProperties)) { + $this->hasBeenModified = false; + } + } else { + $this->hasBeenModified = true; + $this->modifiedProperties[$key] = true; + } + } else { + $this->hasBeenModified = true; + $this->modifiedProperties[$key] = true; + } + + $this->properties[$key] = $value; + return $this; + } + + /** + * Magic getter + * + * @param mixed $key + * + * @return mixed + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Magic setter + * + * @param string $key Key + * @param mixed $val Value + * + * @return void + */ + public function __set($key, $val) + { + $this->set($key, $val); + } + + /** + * Magic isset check + * + * @param string $key + * @return boolean + */ + public function __isset($key) + { + return array_key_exists($key, $this->properties); + } + + /** + * Magic unsetter + * + * @param string $key + * @return void + */ + public function __unset($key) + { + if (! array_key_exists($key, $this->properties)) { + throw new InvalidArgumentException('Trying to unset invalid key'); + } + $this->properties[$key] = $this->defaultProperties[$key]; + } + + /** + * Runs set() for every key/value pair of the given Array + * + * @param array $props Array of properties + * @return self + */ + public function setProperties($props) + { + if (! is_array($props)) { + throw new InvalidArgumentException(sprintf( + 'Array required, got %s', + gettype($props) + )); + } + foreach ($props as $key => $value) { + $this->set($key, $value); + } + return $this; + } + + /** + * Return an array with all object properties + * + * @return array + */ + public function getProperties() + { + //return $this->properties; + $res = array(); + foreach ($this->listProperties() as $key) { + $res[$key] = $this->get($key); + } + + return $res; + } + + protected function getPropertiesForDb() + { + return $this->properties; + } + + public function listProperties() + { + return array_keys($this->properties); + } + + public function getDefaultProperties() + { + return $this->defaultProperties; + } + + /** + * Return all properties that changed since object creation + * + * @return array + */ + public function getModifiedProperties() + { + $props = array(); + foreach (array_keys($this->modifiedProperties) as $key) { + if ($key === $this->autoincKeyName) { + if ($this->protectAutoinc) { + continue; + } elseif ($this->properties[$key] === null) { + continue; + } + } + + $props[$key] = $this->properties[$key]; + } + return $props; + } + + /** + * List all properties that changed since object creation + * + * @return array + */ + public function listModifiedProperties() + { + return array_keys($this->modifiedProperties); + } + + /** + * Whether this object has been modified + * + * @return bool + */ + public function hasBeenModified() + { + return $this->hasBeenModified; + } + + /** + * Whether the given property has been modified + * + * @param string $key Property name + * @return boolean + */ + protected function hasModifiedProperty($key) + { + return array_key_exists($key, $this->modifiedProperties); + } + + /** + * Unique key name + * + * @return string + */ + public function getKeyName() + { + return $this->keyName; + } + + /** + * Autoinc key name + * + * @return string + */ + public function getAutoincKeyName() + { + return $this->autoincKeyName; + } + + /** + * @return ?string + */ + public function getUuidColumn() + { + return $this->uuidColumn; + } + + /** + * @return bool + */ + public function hasUuidColumn() + { + return $this->uuidColumn !== null; + } + + /** + * @return \Ramsey\Uuid\UuidInterface + */ + public function getUniqueId() + { + if ($this->hasUuidColumn()) { + $binaryValue = $this->properties[$this->uuidColumn]; + if (is_resource($binaryValue)) { + throw new RuntimeException('Properties contain binary UUID, probably a programming error'); + } + if ($binaryValue === null) { + $uuid = Uuid::uuid4(); + $this->reallySet($this->uuidColumn, $uuid->getBytes()); + return $uuid; + } + + return Uuid::fromBytes($binaryValue); + } + + throw new InvalidArgumentException(sprintf('%s has no UUID column', $this->getTableName())); + } + + public function getKeyParams() + { + $params = array(); + $key = $this->getKeyName(); + if (is_array($key)) { + foreach ($key as $k) { + $params[$k] = $this->get($k); + } + } else { + $params[$key] = $this->get($this->keyName); + } + + return $params; + } + + /** + * Return the unique identifier + * + * // TODO: may conflict with ->id + * + * @throws InvalidArgumentException When key can not be calculated + * + * @return string|array + */ + public function getId() + { + if (is_array($this->keyName)) { + $id = array(); + foreach ($this->keyName as $key) { + if (isset($this->properties[$key])) { + $id[$key] = $this->properties[$key]; + } + } + + if (empty($id)) { + throw new InvalidArgumentException('Could not evaluate id for multi-column object!'); + } + + return $id; + } else { + if (isset($this->properties[$this->keyName])) { + return $this->properties[$this->keyName]; + } + } + return null; + } + + /** + * Get the autoinc value if set + * + * @return int + */ + public function getAutoincId() + { + if (isset($this->properties[$this->autoincKeyName])) { + return (int) $this->properties[$this->autoincKeyName]; + } + return null; + } + + protected function forgetAutoincId() + { + if (isset($this->properties[$this->autoincKeyName])) { + $this->properties[$this->autoincKeyName] = null; + } + + return $this; + } + + /** + * Liefert das benutzte Datenbank-Handle + * + * @return Zend_Db_Adapter_Abstract + */ + public function getDb() + { + return $this->db; + } + + public function hasConnection() + { + return $this->connection !== null; + } + + public function getConnection() + { + return $this->connection; + } + + /** + * Lädt einen Datensatz aus der Datenbank und setzt die entsprechenden + * Eigenschaften dieses Objekts + * + * @throws NotFoundError + * @return self + */ + protected function loadFromDb() + { + $select = $this->db->select()->from($this->table)->where($this->createWhere()); + $properties = $this->db->fetchRow($select); + + if (empty($properties)) { + if (is_array($this->getKeyName())) { + throw new NotFoundError( + 'Failed to load %s for %s', + $this->table, + $this->createWhere() + ); + } else { + throw new NotFoundError( + 'Failed to load %s "%s"', + $this->table, + $this->getLogId() + ); + } + } + + return $this->setDbProperties($properties); + } + + /** + * @param object|array $row + * @param Db $db + * @return self + */ + public static function fromDbRow($row, Db $db) + { + $self = (new static())->setConnection($db); + if (is_object($row)) { + return $self->setDbProperties((array) $row); + } + + if (is_array($row)) { + return $self->setDbProperties($row); + } + + throw new InvalidDataException('array or object', $row); + } + + protected function setDbProperties($properties) + { + foreach ($properties as $key => $val) { + if (! array_key_exists($key, $this->properties)) { + throw new LogicException(sprintf( + 'Trying to set invalid %s key "%s". DB schema change?', + $this->table, + $key + )); + } + if ($val === null) { + $this->properties[$key] = null; + } elseif (is_resource($val)) { + $this->properties[$key] = stream_get_contents($val); + } else { + $this->properties[$key] = (string) $val; + } + } + + $this->setBeingLoadedFromDb(); + $this->onLoadFromDb(); + return $this; + } + + public function setBeingLoadedFromDb() + { + $this->loadedFromDb = true; + $this->loadedProperties = $this->properties; + $this->hasBeenModified = false; + $this->modifiedProperties = []; + } + + public function setLoadedProperty($key, $value) + { + if ($this->hasBeenLoadedFromDb()) { + $this->set($key, $value); + $this->loadedProperties[$key] = $this->get($key); + } else { + throw new RuntimeException('Cannot set loaded property for new object'); + } + } + + public function getOriginalProperties() + { + return $this->loadedProperties; + } + + public function getOriginalProperty($key) + { + $this->assertPropertyExists($key); + if ($this->hasBeenLoadedFromDb()) { + return $this->loadedProperties[$key]; + } + + return null; + } + + public function resetProperty($key) + { + $this->set($key, $this->getOriginalProperty($key)); + if ($this->listModifiedProperties() === [$key]) { + $this->hasBeenModified = false; + } + + return $this; + } + + public function hasBeenLoadedFromDb() + { + return $this->loadedFromDb; + } + + /** + * Ändert den entsprechenden Datensatz in der Datenbank + * + * @return int Anzahl der geänderten Zeilen + * @throws \Zend_Db_Adapter_Exception + */ + protected function updateDb() + { + $properties = $this->getModifiedProperties(); + if (empty($properties)) { + // Fake true, we might have manually set this to "modified" + return true; + } + $this->quoteBinaryProperties($properties); + + // TODO: Remember changed data for audit and log + return $this->db->update( + $this->table, + $properties, + $this->createWhere() + ); + } + + /** + * Fügt der Datenbank-Tabelle einen entsprechenden Datensatz hinzu + * + * @return int Anzahl der betroffenen Zeilen + * @throws \Zend_Db_Adapter_Exception + */ + protected function insertIntoDb() + { + $properties = $this->getPropertiesForDb(); + if ($this->autoincKeyName !== null) { + if ($this->protectAutoinc || $properties[$this->autoincKeyName] === null) { + unset($properties[$this->autoincKeyName]); + } + } + if ($column = $this->getUuidColumn()) { + $properties[$column] = $this->getUniqueId()->getBytes(); + } + $this->quoteBinaryProperties($properties); + + return $this->db->insert($this->table, $properties); + } + + protected function quoteBinaryProperties(&$properties) + { + foreach ($properties as $key => $value) { + if ($this->isBinaryColumn($key)) { + $properties[$key] = $this->getConnection()->quoteBinary($value); + } + } + } + + protected function isBinaryColumn($column) + { + return in_array($column, $this->binaryProperties) || $this->getUuidColumn() === $column; + } + + /** + * Store object to database + * + * @param DbConnection $db + * @return bool Whether storing succeeded. Always true, throws otherwise + * @throws DuplicateKeyException + */ + public function store(DbConnection $db = null) + { + if ($db !== null) { + $this->setConnection($db); + } + + if ($this->validate() !== true) { + throw new InvalidArgumentException(sprintf( + '%s[%s] validation failed', + $this->table, + $this->getLogId() + )); + } + + if ($this->hasBeenLoadedFromDb() && ! $this->hasBeenModified()) { + return true; + } + + $this->beforeStore(); + $table = $this->table; + $id = $this->getId(); + + try { + if ($this->hasBeenLoadedFromDb()) { + if ($this->updateDb() !== false) { + $this->onUpdate(); + } else { + throw new RuntimeException(sprintf( + 'FAILED storing %s "%s"', + $table, + $this->getLogId() + )); + } + } else { + if ($id && $this->existsInDb()) { + $logId = '"' . $this->getLogId() . '"'; + + if ($autoId = $this->getAutoincId()) { + $logId .= sprintf(', %s=%s', $this->autoincKeyName, $autoId); + } + throw new DuplicateKeyException( + 'Trying to recreate %s (%s)', + $table, + $logId + ); + } + + if ($this->insertIntoDb()) { + if ($this->autoincKeyName && $this->getProperty($this->autoincKeyName) === null) { + if ($this->connection->isPgsql()) { + $this->properties[$this->autoincKeyName] = $this->db->lastInsertId( + $table, + $this->autoincKeyName + ); + } else { + $this->properties[$this->autoincKeyName] = $this->db->lastInsertId(); + } + } + // $this->log(sprintf('New %s "%s" has been stored', $table, $id)); + $this->onInsert(); + } else { + throw new RuntimeException(sprintf( + 'FAILED to store new %s "%s"', + $table, + $this->getLogId() + )); + } + } + } catch (Zend_Db_Exception $e) { + throw new RuntimeException(sprintf( + 'Storing %s[%s] failed: %s {%s}', + $this->table, + $this->getLogId(), + $e->getMessage(), + var_export($this->getProperties(), 1) // TODO: Remove properties + )); + } + + // Hint: order is differs from setBeingLoadedFromDb() as of the onStore hook + $this->modifiedProperties = []; + $this->hasBeenModified = false; + $this->loadedProperties = $this->properties; + $this->onStore(); + $this->loadedFromDb = true; + + return true; + } + + /** + * Delete item from DB + * + * @return int Affected rows + */ + protected function deleteFromDb() + { + return $this->db->delete( + $this->table, + $this->createWhere() + ); + } + + /** + * @param string $key + * @return self + * @throws InvalidArgumentException + */ + protected function setKey($key) + { + $keyname = $this->getKeyName(); + if (is_array($keyname)) { + if (! is_array($key)) { + throw new InvalidArgumentException(sprintf( + '%s has a multicolumn key, array required', + $this->table + )); + } + foreach ($keyname as $k) { + if (! array_key_exists($k, $key)) { + // We allow for null in multicolumn keys: + $key[$k] = null; + } + $this->set($k, $key[$k]); + } + } else { + $this->set($keyname, $key); + } + return $this; + } + + protected function existsInDb() + { + $result = $this->db->fetchRow( + $this->db->select()->from($this->table)->where($this->createWhere()) + ); + return $result !== false; + } + + public function createWhere() + { + if ($this->hasUuidColumn() && $this->properties[$this->uuidColumn] !== null) { + return $this->db->quoteInto( + sprintf('%s = ?', $this->getUuidColumn()), + $this->connection->quoteBinary($this->getUniqueId()->getBytes()) + ); + } + if ($id = $this->getAutoincId()) { + if ($originalId = $this->getOriginalProperty($this->autoincKeyName)) { + return $this->db->quoteInto( + sprintf('%s = ?', $this->autoincKeyName), + $originalId + ); + } + return $this->db->quoteInto( + sprintf('%s = ?', $this->autoincKeyName), + $id + ); + } + + $key = $this->getKeyName(); + + if (is_array($key) && ! empty($key)) { + $where = array(); + foreach ($key as $k) { + if ($this->hasBeenLoadedFromDb()) { + if ($this->loadedProperties[$k] === null) { + $where[] = sprintf('%s IS NULL', $k); + } else { + $where[] = $this->createQuotedWhere($k, $this->loadedProperties[$k]); + } + } else { + if ($this->properties[$k] === null) { + $where[] = sprintf('%s IS NULL', $k); + } else { + $where[] = $this->createQuotedWhere($k, $this->properties[$k]); + } + } + } + + return implode(' AND ', $where); + } else { + if ($this->hasBeenLoadedFromDb()) { + return $this->createQuotedWhere($key, $this->loadedProperties[$key]); + } else { + return $this->createQuotedWhere($key, $this->properties[$key]); + } + } + } + + protected function createQuotedWhere($column, $value) + { + return $this->db->quoteInto( + sprintf('%s = ?', $column), + $this->eventuallyQuoteBinary($value, $column) + ); + } + + protected function eventuallyQuoteBinary($value, $column) + { + if ($this->isBinaryColumn($column)) { + return $this->connection->quoteBinary($value); + } else { + return $value; + } + } + + protected function getLogId() + { + $id = $this->getId(); + if (is_array($id)) { + $logId = json_encode($id); + } else { + $logId = $id; + } + + if ($logId === null && $this->autoincKeyName) { + $logId = $this->getAutoincId(); + } + + return $logId; + } + + public function delete() + { + $table = $this->table; + + if (! $this->hasBeenLoadedFromDb()) { + throw new LogicException(sprintf( + 'Cannot delete %s "%s", it has not been loaded from Db', + $table, + $this->getLogId() + )); + } + + if (! $this->existsInDb()) { + throw new InvalidArgumentException(sprintf( + 'Cannot delete %s "%s", it does not exist', + $table, + $this->getLogId() + )); + } + $this->beforeDelete(); + if (! $this->deleteFromDb()) { + throw new RuntimeException(sprintf( + 'Deleting %s (%s) FAILED', + $table, + $this->getLogId() + )); + } + // $this->log(sprintf('%s "%s" has been DELETED', $table, this->getLogId())); + $this->onDelete(); + $this->loadedFromDb = false; + return true; + } + + public function __clone() + { + $this->onClone(); + $this->forgetAutoincId(); + $this->loadedFromDb = false; + $this->hasBeenModified = true; + } + + protected function onClone() + { + } + + /** + * @param array $properties + * @param DbConnection|null $connection + * + * @return static + */ + public static function create($properties = array(), DbConnection $connection = null) + { + $obj = new static(); + if ($connection !== null) { + $obj->setConnection($connection); + } + $obj->setProperties($properties); + return $obj; + } + + protected static function classWasPrefetched() + { + $class = get_called_class(); + return array_key_exists($class, self::$prefetched); + } + + /** + * @param $key + * @return static|bool + */ + protected static function getPrefetched($key) + { + $class = get_called_class(); + if (static::hasPrefetched($key)) { + if (is_string($key) + && array_key_exists($class, self::$prefetchedNames) + && array_key_exists($key, self::$prefetchedNames[$class]) + ) { + return self::$prefetched[$class][ + self::$prefetchedNames[$class][$key] + ]; + } else { + return self::$prefetched[$class][$key]; + } + } else { + return false; + } + } + + protected static function hasPrefetched($key) + { + $class = get_called_class(); + if (! array_key_exists($class, self::$prefetchStats)) { + self::$prefetchStats[$class] = (object) array( + 'miss' => 0, + 'hits' => 0, + 'hitNames' => 0, + 'combinedMiss' => 0 + ); + } + + if (is_array($key)) { + self::$prefetchStats[$class]->combinedMiss++; + return false; + } + + if (array_key_exists($class, self::$prefetched)) { + if (is_string($key) + && array_key_exists($class, self::$prefetchedNames) + && array_key_exists($key, self::$prefetchedNames[$class]) + ) { + self::$prefetchStats[$class]->hitNames++; + return true; + } elseif (array_key_exists($key, self::$prefetched[$class])) { + self::$prefetchStats[$class]->hits++; + return true; + } else { + self::$prefetchStats[$class]->miss++; + return false; + } + } else { + self::$prefetchStats[$class]->miss++; + return false; + } + } + + public static function getPrefetchStats() + { + return self::$prefetchStats; + } + + /** + * @param $id + * @param DbConnection $connection + * @return static + * @throws NotFoundError + */ + public static function loadWithAutoIncId($id, DbConnection $connection) + { + /* Need to cast to int, otherwise the id will be matched against + * object_name, which may wreak havoc if an object has a + * object_name matching some id. Note that DbObject::set() and + * DbObject::setDbProperties() will convert any property to + * string, including ids. + */ + $id = (int) $id; + + if ($prefetched = static::getPrefetched($id)) { + return $prefetched; + } + + $obj = new static; + if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { + $table = $obj->getTableName(); + assert($connection instanceof Db); + $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); + + return self::$dbObjectStore->load($table, $uuid); + } + + $obj->setConnection($connection) + ->set($obj->autoincKeyName, $id) + ->loadFromDb(); + + return $obj; + } + + /** + * @param $id + * @param DbConnection $connection + * @return static + * @throws NotFoundError + */ + public static function load($id, DbConnection $connection) + { + if ($prefetched = static::getPrefetched($id)) { + return $prefetched; + } + /** @var DbObject $obj */ + $obj = new static; + + if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { + $table = $obj->getTableName(); + assert($connection instanceof Db); + $uuid = UuidLookup::requireUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); + + return self::$dbObjectStore->load($table, $uuid); + } + + $obj->setConnection($connection)->setKey($id)->loadFromDb(); + + return $obj; + } + + /** + * @param DbConnection $connection + * @param \Zend_Db_Select $query + * @param string|null $keyColumn + * + * @return static[] + */ + public static function loadAll(DbConnection $connection, $query = null, $keyColumn = null) + { + $objects = array(); + $db = $connection->getDbAdapter(); + + if ($query === null) { + $dummy = new static; + $select = $db->select()->from($dummy->table); + } else { + $select = $query; + } + $rows = $db->fetchAll($select); + + foreach ($rows as $row) { + /** @var DbObject $obj */ + $obj = new static; + $obj->setConnection($connection)->setDbProperties($row); + if ($keyColumn === null) { + $objects[] = $obj; + } else { + $objects[$row->$keyColumn] = $obj; + } + } + + return $objects; + } + + /** + * @param DbConnection $connection + * @param bool $force + * + * @return static[] + */ + public static function prefetchAll(DbConnection $connection, $force = false) + { + $dummy = static::create(); + $class = get_class($dummy); + $autoInc = $dummy->getAutoincKeyName(); + $keyName = $dummy->getKeyName(); + + if ($force || ! array_key_exists($class, self::$prefetched)) { + self::$prefetched[$class] = static::loadAll($connection, null, $autoInc); + if (! is_array($keyName) && $keyName !== $autoInc) { + foreach (self::$prefetched[$class] as $k => $v) { + self::$prefetchedNames[$class][$v->$keyName] = $k; + } + } + } + + return self::$prefetched[$class]; + } + + public static function clearPrefetchCache() + { + $class = get_called_class(); + if (! array_key_exists($class, self::$prefetched)) { + return; + } + + unset(self::$prefetched[$class]); + unset(self::$prefetchedNames[$class]); + unset(self::$prefetchStats[$class]); + } + + public static function clearAllPrefetchCaches() + { + self::$prefetched = array(); + self::$prefetchedNames = array(); + self::$prefetchStats = array(); + } + + /** + * @param $id + * @param DbConnection $connection + * @return bool + */ + public static function exists($id, DbConnection $connection) + { + if (static::getPrefetched($id)) { + return true; + } elseif (static::classWasPrefetched()) { + return false; + } + + /** @var DbObject $obj */ + $obj = new static; + if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { + $table = $obj->getTableName(); + assert($connection instanceof Db); + $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); + if ($uuid) { + return self::$dbObjectStore->exists($table, $uuid); + } + + return false; + } + + $obj->setConnection($connection)->setKey($id); + return $obj->existsInDb(); + } + + public static function uniqueIdExists(UuidInterface $uuid, DbConnection $connection) + { + $db = $connection->getDbAdapter(); + $obj = new static; + $column = $obj->getUuidColumn(); + $query = $db->select() + ->from($obj->getTableName(), $column) + ->where("$column = ?", $connection->quoteBinary($uuid->getBytes())); + + $result = $db->fetchRow($query); + + return $result !== false; + } + + public static function requireWithUniqueId(UuidInterface $uuid, DbConnection $connection) + { + if ($object = static::loadWithUniqueId($uuid, $connection)) { + return $object; + } + + throw new NotFoundError(sprintf( + 'No %s with UUID=%s has been found', + (new static)->getTableName(), + $uuid->toString() + )); + } + + public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection) + { + $db = $connection->getDbAdapter(); + $obj = new static; + + if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { + $table = $obj->getTableName(); + assert($connection instanceof Db); + return self::$dbObjectStore->load($table, $uuid); + } + + $query = $db->select() + ->from($obj->getTableName()) + ->where($obj->getUuidColumn() . ' = ?', $connection->quoteBinary($uuid->getBytes())); + + $result = $db->fetchRow($query); + + if ($result) { + return $obj->setConnection($connection)->setDbProperties($result); + } + + return null; + } + + public function setUniqueId(UuidInterface $uuid) + { + if ($column = $this->getUuidColumn()) { + $binary = $uuid->getBytes(); + $current = $this->get($column); + if ($current === null) { + $this->set($column, $binary); + } else { + if ($current !== $binary) { + throw new RuntimeException(sprintf( + 'Changing the UUID (from %s to %s) is not allowed', + Uuid::fromBytes($current)->toString(), + Uuid::fromBytes($binary)->toString() + )); + } + } + } + } + + public function __destruct() + { + unset($this->db); + unset($this->connection); + } +} diff --git a/library/Director/Data/Db/DbObjectStore.php b/library/Director/Data/Db/DbObjectStore.php new file mode 100644 index 0000000..bc69b5a --- /dev/null +++ b/library/Director/Data/Db/DbObjectStore.php @@ -0,0 +1,169 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchActivity; +use Icinga\Module\Director\Db\Branch\BranchedObject; +use Icinga\Module\Director\Db\Branch\MergeErrorDeleteMissingObject; +use Icinga\Module\Director\Db\Branch\MergeErrorModificationForMissingObject; +use Icinga\Module\Director\Db\Branch\MergeErrorRecreateOnMerge; +use Icinga\Module\Director\Db\DbUtil; +use Icinga\Module\Director\Objects\IcingaObject; +use Ramsey\Uuid\UuidInterface; + +/** + * Loader for Icinga/DbObjects + * + * Is aware of branches and prefetching. I would prefer to see a StoreInterface, + * with one of the above wrapping the other. But for now, this helps to clean things + * up + */ +class DbObjectStore +{ + /** @var Db */ + protected $connection; + + /** @var ?Branch */ + protected $branch; + + public function __construct(Db $connection, Branch $branch = null) + { + $this->connection = $connection; + $this->branch = $branch; + } + + /** + * @param $tableName + * @param UuidInterface $uuid + * @return DbObject|null + * @throws \Icinga\Exception\NotFoundError + */ + public function load($tableName, UuidInterface $uuid) + { + $branchedObject = BranchedObject::load($this->connection, $tableName, $uuid, $this->branch); + $object = $branchedObject->getBranchedDbObject($this->connection); + if ($object === null) { + return null; + } + + $object->setBeingLoadedFromDb(); + + return $object; + } + + /** + * @param string $tableName + * @param string $arrayIdx + * @return DbObject[]|IcingaObject[] + * @throws MergeErrorRecreateOnMerge + * @throws MergeErrorDeleteMissingObject + * @throws MergeErrorModificationForMissingObject + */ + public function loadAll($tableName, $arrayIdx = 'uuid') + { + $db = $this->connection->getDbAdapter(); + $class = DbObjectTypeRegistry::classByType($tableName); + $query = $db->select()->from($tableName)->order($arrayIdx); + $result = []; + foreach ($db->fetchAll($query) as $row) { + $row->uuid = DbUtil::binaryResult($row->uuid); + $result[$row->uuid] = $class::create((array) $row, $this->connection); + $result[$row->uuid]->setBeingLoadedFromDb(); + } + if ($this->branch && $this->branch->isBranch()) { + $query = $db->select() + ->from(BranchActivity::DB_TABLE) + ->where('branch_uuid = ?', $this->connection->quoteBinary($this->branch->getUuid()->getBytes())) + ->order('timestamp_ns ASC'); + $rows = $db->fetchAll($query); + foreach ($rows as $row) { + $activity = BranchActivity::fromDbRow($row); + if ($activity->getObjectTable() !== $tableName) { + continue; + } + $uuid = $activity->getObjectUuid(); + $binaryUuid = $uuid->getBytes(); + + $exists = isset($result[$binaryUuid]); + if ($activity->isActionCreate()) { + if ($exists) { + throw new MergeErrorRecreateOnMerge($activity); + } else { + $new = $activity->createDbObject($this->connection); + $new->setBeingLoadedFromDb(); + $result[$binaryUuid] = $new; + } + } elseif ($activity->isActionDelete()) { + if ($exists) { + unset($result[$binaryUuid]); + } else { + throw new MergeErrorDeleteMissingObject($activity); + } + } else { + if ($exists) { + $activity->applyToDbObject($result[$binaryUuid])->setBeingLoadedFromDb(); + } else { + throw new MergeErrorModificationForMissingObject($activity); + } + } + } + } + + if ($arrayIdx === 'uuid') { + return $result; + } + + $indexedResult = []; + foreach ($result as $object) { + $indexedResult[$object->get($arrayIdx)] = $object; + } + + return $indexedResult; + } + + public function exists($tableName, UuidInterface $uuid) + { + return BranchedObject::exists($this->connection, $tableName, $uuid, $this->branch->getUuid()); + } + + public function store(DbObject $object) + { + if ($this->branch && $this->branch->isBranch()) { + $activity = BranchActivity::forDbObject($object, $this->branch); + $this->connection->runFailSafeTransaction(function () use ($activity) { + $activity->store($this->connection); + BranchedObject::withActivity($activity, $this->connection)->store($this->connection); + }); + + return true; + } else { + return $object->store($this->connection); + } + } + + public function delete(DbObject $object) + { + if ($this->branch && $this->branch->isBranch()) { + $activity = BranchActivity::deleteObject($object, $this->branch); + $this->connection->runFailSafeTransaction(function () use ($activity) { + $activity->store($this->connection); + BranchedObject::load( + $this->connection, + $activity->getObjectTable(), + $activity->getObjectUuid(), + $this->branch + )->delete($this->connection); + }); + return true; + } + + return $object->delete(); + } + + public function getBranch() + { + return $this->branch; + } +} diff --git a/library/Director/Data/Db/DbObjectTypeRegistry.php b/library/Director/Data/Db/DbObjectTypeRegistry.php new file mode 100644 index 0000000..0c226d6 --- /dev/null +++ b/library/Director/Data/Db/DbObjectTypeRegistry.php @@ -0,0 +1,75 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaObject; + +class DbObjectTypeRegistry +{ + /** + * @param $type + * @return string|DbObject Fake typehint for IDE + */ + public static function classByType($type) + { + // allow for icinga_host and host + $type = lcfirst(preg_replace('/^icinga_/', '', $type)); + + // Hint: Sync/Import are not IcingaObjects, this should be reconsidered: + if (strpos($type, 'import') === 0 || strpos($type, 'sync') === 0) { + $prefix = ''; + } elseif (strpos($type, 'data') === false) { + $prefix = 'Icinga'; + } else { + $prefix = 'Director'; + } + + // TODO: Provide a more sophisticated solution + if ($type === 'hostgroup') { + $type = 'hostGroup'; + } elseif ($type === 'usergroup') { + $type = 'userGroup'; + } elseif ($type === 'timeperiod') { + $type = 'timePeriod'; + } elseif ($type === 'servicegroup') { + $type = 'serviceGroup'; + } elseif ($type === 'service_set' || $type === 'serviceset') { + $type = 'serviceSet'; + } elseif ($type === 'apiuser') { + $type = 'apiUser'; + } elseif ($type === 'host_template_choice') { + $type = 'templateChoiceHost'; + } elseif ($type === 'service_template_choice') { + $type = 'TemplateChoiceService'; + } elseif ($type === 'scheduled_downtime' || $type === 'scheduled-downtime') { + $type = 'ScheduledDowntime'; + } + + return 'Icinga\\Module\\Director\\Objects\\' . $prefix . ucfirst($type); + } + + public static function tableNameByType($type) + { + $class = static::classByType($type); + $dummy = $class::create([]); + + return $dummy->getTableName(); + } + + public static function shortTypeForObject(DbObject $object) + { + if ($object instanceof IcingaObject) { + return $object->getShortTableName(); + } + + return $object->getTableName(); + } + + public static function newObject($type, $properties = [], Db $db = null) + { + /** @var DbObject $class fake hint for the IDE, it's a string */ + $class = self::classByType($type); + return $class::create($properties, $db); + } +} diff --git a/library/Director/Data/Db/DbObjectWithSettings.php b/library/Director/Data/Db/DbObjectWithSettings.php new file mode 100644 index 0000000..4f6b139 --- /dev/null +++ b/library/Director/Data/Db/DbObjectWithSettings.php @@ -0,0 +1,168 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Module\Director\Db; + +abstract class DbObjectWithSettings extends DbObject +{ + /** @var Db $connection */ + protected $connection; + + protected $settingsTable = 'your_table_name'; + + protected $settingsRemoteId = 'column_pointing_to_main_table_id'; + + protected $settings = []; + + public function set($key, $value) + { + if ($this->hasProperty($key)) { + return parent::set($key, $value); + } elseif ($this->hasSetterForProperty($key)) { // Hint: hasProperty checks only for Getters + return parent::set($key, $value); + } + + if (! \array_key_exists($key, $this->settings) || $value !== $this->settings[$key]) { + $this->hasBeenModified = true; + } + + $this->settings[$key] = $value; + return $this; + } + + public function get($key) + { + if ($this->hasProperty($key)) { + return parent::get($key); + } + + if (array_key_exists($key, $this->settings)) { + return $this->settings[$key]; + } + + return parent::get($key); + } + + public function setSettings($settings) + { + $settings = (array) $settings; + ksort($settings); + if ($settings !== $this->settings) { + $this->settings = $settings; + $this->hasBeenModified = true; + } + + return $this; + } + + public function getSettings() + { + // Sort them, important only for new objects + ksort($this->settings); + return $this->settings; + } + + public function getSetting($name, $default = null) + { + if (array_key_exists($name, $this->settings)) { + return $this->settings[$name]; + } + + return $default; + } + + public function getStoredSetting($name, $default = null) + { + $stored = $this->fetchSettingsFromDb(); + if (array_key_exists($name, $stored)) { + return $stored[$name]; + } + + return $default; + } + + public function __unset($key) + { + if ($this->hasProperty($key)) { + parent::__unset($key); + } + + if (array_key_exists($key, $this->settings)) { + unset($this->settings[$key]); + $this->hasBeenModified = true; + } + } + + protected function onStore() + { + $old = $this->fetchSettingsFromDb(); + $oldKeys = array_keys($old); + $newKeys = array_keys($this->settings); + $add = []; + $mod = []; + $del = []; + $id = $this->get('id'); + + foreach ($this->settings as $key => $val) { + if (array_key_exists($key, $old)) { + if ($old[$key] !== $this->settings[$key]) { + $mod[$key] = $this->settings[$key]; + } + } else { + $add[$key] = $this->settings[$key]; + } + } + + foreach (array_diff($oldKeys, $newKeys) as $key) { + $del[] = $key; + } + + $where = sprintf($this->settingsRemoteId . ' = %d AND setting_name = ?', $id); + $db = $this->getDb(); + foreach ($mod as $key => $val) { + $db->update( + $this->settingsTable, + ['setting_value' => $val], + $db->quoteInto($where, $key) + ); + } + + foreach ($add as $key => $val) { + $db->insert( + $this->settingsTable, + [ + $this->settingsRemoteId => $id, + 'setting_name' => $key, + 'setting_value' => $val + ] + ); + } + + if (! empty($del)) { + $where = sprintf($this->settingsRemoteId . ' = %d AND setting_name IN (?)', $id); + $db->delete($this->settingsTable, $db->quoteInto($where, $del)); + } + } + + protected function fetchSettingsFromDb() + { + $db = $this->getDb(); + $id = $this->get('id'); + if (! $id) { + return []; + } + + return $db->fetchPairs( + $db->select() + ->from($this->settingsTable, ['setting_name', 'setting_value']) + ->where($this->settingsRemoteId . ' = ?', $id) + ->order('setting_name') + ); + } + + protected function onLoadFromDb() + { + $this->settings = $this->fetchSettingsFromDb(); + } +} diff --git a/library/Director/Data/Db/IcingaObjectFilterRenderer.php b/library/Director/Data/Db/IcingaObjectFilterRenderer.php new file mode 100644 index 0000000..de2ec79 --- /dev/null +++ b/library/Director/Data/Db/IcingaObjectFilterRenderer.php @@ -0,0 +1,133 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterException; +use Icinga\Data\Filter\FilterExpression; + +class IcingaObjectFilterRenderer +{ + /** @var Filter */ + protected $filter; + + /** @var IcingaObjectQuery */ + protected $query; + + protected $columnMap = [ + 'host.name' => 'host.object_name', + 'service.name' => 'service.object_name', + ]; + + public function __construct(Filter $filter, IcingaObjectQuery $query) + { + $this->filter = clone($filter); + $this->fixFilterColumns($this->filter); + $this->query = $query; + } + + /** + * @param Filter $filter + * @param IcingaObjectQuery $query + * + * @return IcingaObjectQuery + */ + public static function apply(Filter $filter, IcingaObjectQuery $query) + { + $self = new static($filter, $query); + return $self->applyFilterToQuery(); + } + + /** + * @return IcingaObjectQuery + */ + protected function applyFilterToQuery() + { + $this->query->escapedWhere($this->renderFilter($this->filter)); + return $this->query; + } + + /** + * @param Filter $filter + * @return string + */ + protected function renderFilter(Filter $filter) + { + if ($filter->isChain()) { + /** @var FilterChain $filter */ + return $this->renderFilterChain($filter); + } else { + /** @var FilterExpression $filter */ + return $this->renderFilterExpression($filter); + } + } + + /** + * @param FilterChain $filter + * + * @throws FilterException + * + * @return string + */ + protected function renderFilterChain(FilterChain $filter) + { + $parts = array(); + foreach ($filter->filters() as $sub) { + $parts[] = $this->renderFilter($sub); + } + + $op = $filter->getOperatorName(); + if ($op === 'NOT') { + if (count($parts) !== 1) { + throw new FilterException( + 'NOT should have exactly one child, got %s', + count($parts) + ); + } + + return $op . ' ' . $parts[0]; + } else { + if ($filter->isRootNode()) { + return implode(' ' . $op . ' ', $parts); + } else { + return '(' . implode(' ' . $op . ' ', $parts) . ')'; + } + } + } + + protected function fixFilterColumns(Filter $filter) + { + if ($filter->isExpression()) { + /** @var FilterExpression $filter */ + $col = $filter->getColumn(); + if (array_key_exists($col, $this->columnMap)) { + $filter->setColumn($this->columnMap[$col]); + } + if (strpos($col, 'vars.') === false) { + $filter->setExpression(json_decode($filter->getExpression())); + } + } else { + /** @var FilterChain $filter */ + foreach ($filter->filters() as $sub) { + $this->fixFilterColumns($sub); + } + } + } + + /** + * @param FilterExpression $filter + * + * @return string + */ + protected function renderFilterExpression(FilterExpression $filter) + { + $query = $this->query; + $column = $query->getAliasForRequiredFilterColumn($filter->getColumn()); + return $query->whereToSql( + $column, + $filter->getSign(), + $filter->getExpression() + ); + } +} diff --git a/library/Director/Data/Db/IcingaObjectQuery.php b/library/Director/Data/Db/IcingaObjectQuery.php new file mode 100644 index 0000000..4556ba7 --- /dev/null +++ b/library/Director/Data/Db/IcingaObjectQuery.php @@ -0,0 +1,255 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Data\Db\DbQuery; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\NotImplementedError; +use Icinga\Module\Director\Db; +use Zend_Db_Expr as ZfDbExpr; +use Zend_Db_Select as ZfDbSelect; + +class IcingaObjectQuery +{ + const BASE_ALIAS = 'o'; + + /** @var Db */ + protected $connection; + + /** @var string */ + protected $type; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** @var ZfDbSelect */ + protected $query; + + /** @var bool */ + protected $resolved; + + /** @var array joined tables, alias => table */ + protected $requiredTables; + + /** @var array maps table aliases, alias => table*/ + protected $aliases; + + /** @var DbQuery */ + protected $dummyQuery; + + /** @var array varname => alias */ + protected $joinedVars = array(); + + protected $customVarTable; + + protected $baseQuery; + + /** + * IcingaObjectQuery constructor. + * + * @param string $type + * @param Db $connection + * @param bool $resolved + */ + public function __construct($type, Db $connection, $resolved = true) + { + $this->type = $type; + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + $this->resolved = $resolved; + $baseTable = 'icinga_' . $type; + $this->baseQuery = $this->db->select() + ->from( + array(self::BASE_ALIAS => $baseTable), + array('name' => 'object_name') + )->order(self::BASE_ALIAS . '.object_name'); + } + + public function joinVar($name) + { + if (! $this->hasJoinedVar($name)) { + $type = $this->type; + $alias = $this->safeVarAlias($name); + $varAlias = "v_$alias"; + // TODO: optionally $varRelation = sprintf('icinga_%s_resolved_var', $type); + $varRelation = sprintf('icinga_%s_var', $type); + $idCol = sprintf('%s.%s_id', $alias, $type); + + $joinOn = sprintf('%s = %s.id', $idCol, self::BASE_ALIAS); + $joinVarOn = $this->db->quoteInto( + sprintf('%s.checksum = %s.checksum AND %s.varname = ?', $alias, $varAlias, $alias), + $name + ); + + $this->baseQuery->join( + array($alias => $varRelation), + $joinOn, + array() + )->join( + array($varAlias => 'icinga_var'), + $joinVarOn, + array($alias => $varAlias . '.varvalue') + ); + + $this->joinedVars[$name] = $varAlias . '.varvalue'; + } + + return $this; + } + + // Debug only + public function getSql() + { + return (string) $this->baseQuery; + } + + public function listNames() + { + return $this->db->fetchCol( + $this->baseQuery + ); + } + + protected function hasJoinedVar($name) + { + return array_key_exists($name, $this->joinedVars); + } + + public function getJoinedVarAlias($name) + { + return $this->joinedVars[$name]; + } + + // TODO: recheck this + protected function safeVarAlias($name) + { + $alias = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $name); + $cnt = 1; + $checkAlias = $alias; + while (in_array($checkAlias, $this->joinedVars)) { + $cnt++; + $checkAlias = $alias . '_' . $cnt; + } + + return $checkAlias; + } + + public function escapedWhere($where) + { + $this->baseQuery->where(new ZfDbExpr($where)); + } + + /** + * @param $column + * @return string + * @throws NotFoundError + * @throws NotImplementedError + */ + public function getAliasForRequiredFilterColumn($column) + { + list($key, $sub) = $this->splitFilterKey($column); + if ($sub === null) { + return $key; + } else { + $objectType = $key; + } + + if ($objectType === $this->type) { + list($key, $sub) = $this->splitFilterKey($sub); + if ($sub === null) { + return $key; + } + + if ($key === 'vars') { + return $this->joinVar($sub)->getJoinedVarAlias($sub); + } else { + throw new NotFoundError('Not yet, my type: %s - %s', $objectType, $key); + } + } else { + throw new NotImplementedError('Not yet: %s - %s', $objectType, $sub); + } + } + + protected function splitFilterKey($key) + { + $dot = strpos($key, '.'); + if ($dot === false) { + return [$key, null]; + } else { + return [substr($key, 0, $dot), substr($key, $dot + 1)]; + } + } + + protected function requireTable($name) + { + if ($alias = $this->getTableAliasFromQuery($name)) { + return $alias; + } + + $this->joinTable($name); + } + + protected function joinTable($name) + { + if (!array_key_exists($name, $this->requiredTables)) { + $alias = $this->makeAlias($name); + } + + return $this->tableAliases($name); + } + + protected function hasAlias($name) + { + return array_key_exists($name, $this->aliases); + } + + protected function makeAlias($name) + { + if (substr($name, 0, 7) === 'icinga_') { + $shortName = substr($name, 7); + } else { + $shortName = $name; + } + + $parts = preg_split('/_/', $shortName, -1); + $alias = ''; + foreach ($parts as $part) { + $alias .= $part[0]; + if (! $this->hasAlias($alias)) { + return $alias; + } + } + + $cnt = 1; + do { + $cnt++; + if (! $this->hasAlias($alias . $cnt)) { + return $alias . $cnt; + } + } while (! $this->hasAlias($alias)); + + return $alias; + } + + protected function getTableAliasFromQuery($table) + { + $tables = $this->query->getPart('from'); + $key = array_search($table, $tables); + if ($key === null || $key === false) { + return false; + } + /* + 'joinType' => $type, + 'schema' => $schema, + 'tableName' => $tableName, + 'joinCondition' => $cond + */ + return $key; + } + + public function whereToSql($col, $sign, $expression) + { + return $this->connection->renderFilter(Filter::expression($col, $sign, $expression)); + } +} diff --git a/library/Director/Data/Db/ServiceSetQueryBuilder.php b/library/Director/Data/Db/ServiceSetQueryBuilder.php new file mode 100644 index 0000000..7841d1e --- /dev/null +++ b/library/Director/Data/Db/ServiceSetQueryBuilder.php @@ -0,0 +1,158 @@ +<?php + +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\BranchSupport; +use Icinga\Module\Director\Db\DbSelectParenthesis; +use Icinga\Module\Director\Db\DbUtil; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Web\Table\TableWithBranchSupport; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; + +class ServiceSetQueryBuilder +{ + use TableWithBranchSupport; + + const TABLE = BranchSupport::TABLE_ICINGA_SERVICE; + const BRANCHED_TABLE = BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE; + const SET_TABLE = BranchSupport::TABLE_ICINGA_SERVICE_SET; + const BRANCHED_SET_TABLE = BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE_SET; + + /** @var Db */ + protected $connection; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** + * @param ?UuidInterface $uuid + */ + public function __construct(Db $connection, $uuid = null) + { + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + if ($uuid) { + $this->setBranchUuid($uuid); + } + } + + /** + * @return \Zend_Db_Select + * @throws \Zend_Db_Select_Exception + */ + public function selectServicesForSet(IcingaServiceSet $set) + { + $db = $this->connection->getDbAdapter(); + if ($this->branchUuid) { + $right = $this->selectRightBranchedServices($set)->columns($this->getRightBranchedColumns()); + $left = $this->selectLeftBranchedServices($set)->columns($this->getLeftBranchedColumns()); + $query = $this->db->select()->from(['u' => $db->select()->union([ + 'l' => new DbSelectParenthesis($left), + 'r' => new DbSelectParenthesis($right), + ])]); + $query->order('service_set'); + } else { + $query = $this->selectServices($set)->columns($this->getColumns()); + } + + return $query; + } + + protected function selectServices(IcingaServiceSet $set) + { + return $this->db + ->select() + ->from(['o' =>self::TABLE], []) + ->joinLeft(['os' => self::SET_TABLE], 'os.id = o.service_set_id', []) + ->where('os.uuid = ?', $this->connection->quoteBinary($set->getUniqueId()->getBytes())); + } + + protected function selectLeftBranchedServices(IcingaServiceSet $set) + { + return $this + ->selectServices($set) + ->joinLeft( + ['bo' => self::BRANCHED_TABLE], + $this->db->quoteInto('bo.uuid = o.uuid AND bo.branch_uuid = ?', $this->getQuotedBranchUuid()), + [] + ); + } + + protected function selectRightBranchedServices(IcingaServiceSet $set) + { + return $this->db + ->select() + ->from(['o' => self::TABLE], []) + ->joinRight(['bo' => self::BRANCHED_TABLE], 'bo.uuid = o.uuid', []) + ->where('bo.service_set = ?', $set->get('object_name')) + ->where('bo.branch_uuid = ?', $this->getQuotedBranchUuid()); + } + + protected static function resetQueryProperties(\Zend_Db_Select $query) + { + // TODO: Keep existing UUID, becomes important when using this for other tables too (w/o UNION) + // $columns = $query->getPart($query::COLUMNS); + $query->reset($query::COLUMNS); + $query->columns('uuid'); + return $query; + } + + public function fetchServicesWithQuery(\Zend_Db_Select $query) + { + static::resetQueryProperties($query); + $db = $this->connection->getDbAdapter(); + $uuids = $db->fetchCol($query); + + $services = []; + foreach ($uuids as $uuid) { + $service = IcingaService::loadWithUniqueId(Uuid::fromBytes(DbUtil::binaryResult($uuid)), $this->connection); + $service->set('service_set', null); // TODO: CHECK THIS!!!! + + $services[$service->getObjectName()] = $service; + } + + return $services; + } + + protected function getColumns() + { + return [ + 'uuid' => 'o.uuid', // MUST be first because of UNION column order, see branchifyColumns() + 'id' => 'o.id', + 'branch_uuid' => '(null)', + 'service_set' => 'os.object_name', + 'service' => 'o.object_name', + 'disabled' => 'o.disabled', + 'object_type' => 'o.object_type', + 'blacklisted' => "('n')", + ]; + } + + protected function getLeftBranchedColumns() + { + $columns = $this->getColumns(); + $columns['branch_uuid'] = 'bo.branch_uuid'; + $columns['service_set'] = 'COALESCE(os.object_name, bo.service_set)'; + + return $this->branchifyColumns($columns); + } + + protected function getRightBranchedColumns() + { + $columns = $this->getColumns(); + $columns = $this->branchifyColumns($columns); + $columns['branch_uuid'] = 'bo.branch_uuid'; + $columns['service_set'] = 'bo.service_set'; + $columns['id'] = '(NULL)'; + + return $columns; + } + + protected function getQuotedBranchUuid() + { + return $this->connection->quoteBinary($this->branchUuid->getBytes()); + } +} |