summaryrefslogtreecommitdiffstats
path: root/library/Director/Data/Db
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:43:12 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:43:12 +0000
commitcd989f9c3aff968e19a3aeabc4eb9085787a6673 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Data/Db
parentInitial commit. (diff)
downloadicingaweb2-module-director-cd989f9c3aff968e19a3aeabc4eb9085787a6673.tar.xz
icingaweb2-module-director-cd989f9c3aff968e19a3aeabc4eb9085787a6673.zip
Adding upstream version 1.10.2.upstream/1.10.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Director/Data/Db')
-rw-r--r--library/Director/Data/Db/DbConnection.php51
-rw-r--r--library/Director/Data/Db/DbDataFormatter.php26
-rw-r--r--library/Director/Data/Db/DbObject.php1487
-rw-r--r--library/Director/Data/Db/DbObjectStore.php169
-rw-r--r--library/Director/Data/Db/DbObjectTypeRegistry.php75
-rw-r--r--library/Director/Data/Db/DbObjectWithSettings.php168
-rw-r--r--library/Director/Data/Db/IcingaObjectFilterRenderer.php133
-rw-r--r--library/Director/Data/Db/IcingaObjectQuery.php255
-rw-r--r--library/Director/Data/Db/ServiceSetQueryBuilder.php158
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());
+ }
+}