1<?php 2 3namespace Icinga\Module\Director\Objects; 4 5use Icinga\Application\Benchmark; 6use Icinga\Exception\NotFoundError; 7use Icinga\Module\Director\Data\Db\DbObjectWithSettings; 8use Icinga\Module\Director\Db; 9use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; 10use Icinga\Module\Director\Exception\DuplicateKeyException; 11use Icinga\Module\Director\Hook\PropertyModifierHook; 12use Icinga\Module\Director\Import\Import; 13use Icinga\Module\Director\Import\SyncUtils; 14use InvalidArgumentException; 15use Exception; 16 17class ImportSource extends DbObjectWithSettings implements ExportInterface 18{ 19 protected $table = 'import_source'; 20 21 protected $keyName = 'source_name'; 22 23 protected $autoincKeyName = 'id'; 24 25 protected $protectAutoinc = false; 26 27 protected $defaultProperties = [ 28 'id' => null, 29 'source_name' => null, 30 'provider_class' => null, 31 'key_column' => null, 32 'import_state' => 'unknown', 33 'last_error_message' => null, 34 'last_attempt' => null, 35 'description' => null, 36 ]; 37 38 protected $stateProperties = [ 39 'import_state', 40 'last_error_message', 41 'last_attempt', 42 ]; 43 44 protected $settingsTable = 'import_source_setting'; 45 46 protected $settingsRemoteId = 'source_id'; 47 48 private $rowModifiers; 49 50 private $newRowModifiers; 51 52 /** 53 * @return \stdClass 54 */ 55 public function export() 56 { 57 $plain = $this->getProperties(); 58 $plain['originalId'] = $plain['id']; 59 unset($plain['id']); 60 61 foreach ($this->stateProperties as $key) { 62 unset($plain[$key]); 63 } 64 65 $plain['settings'] = (object) $this->getSettings(); 66 $plain['modifiers'] = $this->exportRowModifiers(); 67 ksort($plain); 68 69 return (object) $plain; 70 } 71 72 /** 73 * @param $plain 74 * @param Db $db 75 * @param bool $replace 76 * @return ImportSource 77 * @throws DuplicateKeyException 78 * @throws NotFoundError 79 */ 80 public static function import($plain, Db $db, $replace = false) 81 { 82 $properties = (array) $plain; 83 if (isset($properties['originalId'])) { 84 $id = $properties['originalId']; 85 unset($properties['originalId']); 86 } else { 87 $id = null; 88 } 89 $name = $properties['source_name']; 90 91 if ($replace && static::existsWithNameAndId($name, $id, $db)) { 92 $object = static::loadWithAutoIncId($id, $db); 93 } elseif ($replace && static::exists($name, $db)) { 94 $object = static::load($name, $db); 95 } elseif (static::existsWithName($name, $db)) { 96 throw new DuplicateKeyException( 97 'Import Source %s already exists', 98 $name 99 ); 100 } else { 101 $object = static::create([], $db); 102 } 103 104 $object->newRowModifiers = $properties['modifiers']; 105 unset($properties['modifiers']); 106 $object->setProperties($properties); 107 if ($id !== null) { 108 $object->reallySet('id', $id); 109 } 110 111 return $object; 112 } 113 114 public function getUniqueIdentifier() 115 { 116 return $this->get('source_name'); 117 } 118 119 /** 120 * @param $name 121 * @param Db $connection 122 * @return ImportSource 123 * @throws NotFoundError 124 */ 125 public static function loadByName($name, Db $connection) 126 { 127 $db = $connection->getDbAdapter(); 128 $properties = $db->fetchRow( 129 $db->select()->from('import_source')->where('source_name = ?', $name) 130 ); 131 if ($properties === false) { 132 throw new NotFoundError(sprintf( 133 'There is no such Import Source: "%s"', 134 $name 135 )); 136 } 137 138 return static::create([], $connection)->setDbProperties($properties); 139 } 140 141 public static function existsWithName($name, Db $connection) 142 { 143 $db = $connection->getDbAdapter(); 144 145 return (string) $name === (string) $db->fetchOne( 146 $db->select() 147 ->from('import_source', 'source_name') 148 ->where('source_name = ?', $name) 149 ); 150 } 151 152 /** 153 * @param string $name 154 * @param int $id 155 * @param Db $connection 156 * @api internal 157 * @return bool 158 */ 159 protected static function existsWithNameAndId($name, $id, Db $connection) 160 { 161 $db = $connection->getDbAdapter(); 162 $dummy = new static; 163 $idCol = $dummy->autoincKeyName; 164 $keyCol = $dummy->keyName; 165 166 return (string) $id === (string) $db->fetchOne( 167 $db->select() 168 ->from($dummy->table, $idCol) 169 ->where("$idCol = ?", $id) 170 ->where("$keyCol = ?", $name) 171 ); 172 } 173 174 protected function exportRowModifiers() 175 { 176 $modifiers = []; 177 foreach ($this->fetchRowModifiers() as $modifier) { 178 $modifiers[] = $modifier->export(); 179 } 180 181 return $modifiers; 182 } 183 184 /** 185 * @param bool $required 186 * @return ImportRun|null 187 * @throws NotFoundError 188 */ 189 public function fetchLastRun($required = false) 190 { 191 return $this->fetchLastRunBefore(time() + 1, $required); 192 } 193 194 /** 195 * @throws DuplicateKeyException 196 */ 197 protected function onStore() 198 { 199 parent::onStore(); 200 if ($this->newRowModifiers !== null) { 201 $connection = $this->getConnection(); 202 $db = $connection->getDbAdapter(); 203 $myId = $this->get('id'); 204 if ($this->hasBeenLoadedFromDb()) { 205 $db->delete( 206 'import_row_modifier', 207 $db->quoteInto('source_id = ?', $myId) 208 ); 209 } 210 211 foreach ($this->newRowModifiers as $modifier) { 212 $modifier = ImportRowModifier::create((array) $modifier, $connection); 213 $modifier->set('source_id', $myId); 214 $modifier->store(); 215 } 216 } 217 } 218 219 /** 220 * @param $timestamp 221 * @param bool $required 222 * @return ImportRun|null 223 * @throws NotFoundError 224 */ 225 public function fetchLastRunBefore($timestamp, $required = false) 226 { 227 if (! $this->hasBeenLoadedFromDb()) { 228 return $this->nullUnlessRequired($required); 229 } 230 231 if ($timestamp === null) { 232 $timestamp = time(); 233 } 234 235 $db = $this->getDb(); 236 $query = $db->select()->from( 237 ['ir' => 'import_run'], 238 'ir.id' 239 )->where('ir.source_id = ?', $this->get('id')) 240 ->where('ir.start_time < ?', date('Y-m-d H:i:s', $timestamp)) 241 ->order('ir.start_time DESC') 242 ->limit(1); 243 244 $runId = $db->fetchOne($query); 245 246 if ($runId) { 247 return ImportRun::load($runId, $this->getConnection()); 248 } else { 249 return $this->nullUnlessRequired($required); 250 } 251 } 252 253 /** 254 * @param $required 255 * @return null 256 * @throws NotFoundError 257 */ 258 protected function nullUnlessRequired($required) 259 { 260 if ($required) { 261 throw new NotFoundError( 262 'No data has been imported for "%s" yet', 263 $this->get('source_name') 264 ); 265 } 266 267 return null; 268 } 269 270 public function applyModifiers(& $data) 271 { 272 $modifiers = $this->fetchFlatRowModifiers(); 273 274 if (empty($modifiers)) { 275 return $this; 276 } 277 278 279 foreach ($modifiers as $modPair) { 280 /** @var PropertyModifierHook $modifier */ 281 list($property, $modifier) = $modPair; 282 $rejected = []; 283 foreach ($data as $key => $row) { 284 $this->applyPropertyModifierToRow($modifier, $property, $row); 285 if ($modifier->rejectsRow()) { 286 $rejected[] = $key; 287 $modifier->rejectRow(false); 288 } 289 } 290 291 foreach ($rejected as $key) { 292 unset($data[$key]); 293 } 294 } 295 296 return $this; 297 } 298 299 public function getObjectName() 300 { 301 return $this->get('source_name'); 302 } 303 304 public static function getKeyColumnName() 305 { 306 return 'source_name'; 307 } 308 309 protected function applyPropertyModifierToRow(PropertyModifierHook $modifier, $key, $row) 310 { 311 if ($modifier->requiresRow()) { 312 $modifier->setRow($row); 313 } 314 315 if (property_exists($row, $key)) { 316 $value = $row->$key; 317 } elseif (strpos($key, '.') !== false) { 318 $value = SyncUtils::getSpecificValue($row, $key); 319 } else { 320 $value = null; 321 } 322 323 $target = $modifier->getTargetProperty($key); 324 if (strpos($target, '.') !== false) { 325 throw new InvalidArgumentException( 326 'Cannot set value for nested key "%s"', 327 $target 328 ); 329 } 330 331 if (is_array($value) && ! $modifier->hasArraySupport()) { 332 $new = []; 333 foreach ($value as $k => $v) { 334 $new[$k] = $modifier->transform($v); 335 } 336 $row->$target = $new; 337 } else { 338 $row->$target = $modifier->transform($value); 339 } 340 } 341 342 public function getRowModifiers() 343 { 344 if ($this->rowModifiers === null) { 345 $this->prepareRowModifiers(); 346 } 347 348 return $this->rowModifiers; 349 } 350 351 public function hasRowModifiers() 352 { 353 return count($this->getRowModifiers()) > 0; 354 } 355 356 /** 357 * @return ImportRowModifier[] 358 */ 359 public function fetchRowModifiers() 360 { 361 $db = $this->getDb(); 362 363 $modifiers = ImportRowModifier::loadAll( 364 $this->getConnection(), 365 $db->select() 366 ->from('import_row_modifier') 367 ->where('source_id = ?', $this->get('id')) 368 ->order('priority ASC') 369 ); 370 371 return $modifiers; 372 } 373 374 protected function fetchFlatRowModifiers() 375 { 376 $mods = []; 377 foreach ($this->fetchRowModifiers() as $mod) { 378 $mods[] = [$mod->get('property_name'), $mod->getInstance()]; 379 } 380 381 return $mods; 382 } 383 384 protected function prepareRowModifiers() 385 { 386 $modifiers = []; 387 388 foreach ($this->fetchRowModifiers() as $mod) { 389 $name = $mod->get('property_name'); 390 if (! array_key_exists($name, $modifiers)) { 391 $modifiers[$name] = []; 392 } 393 394 $modifiers[$name][] = $mod->getInstance(); 395 } 396 397 $this->rowModifiers = $modifiers; 398 } 399 400 public function listModifierTargetProperties() 401 { 402 $list = []; 403 foreach ($this->getRowModifiers() as $rowMods) { 404 /** @var PropertyModifierHook $mod */ 405 foreach ($rowMods as $mod) { 406 if ($mod->hasTargetProperty()) { 407 $list[$mod->getTargetProperty()] = true; 408 } 409 } 410 } 411 412 return array_keys($list); 413 } 414 415 /** 416 * @param bool $runImport 417 * @return bool 418 * @throws DuplicateKeyException 419 */ 420 public function checkForChanges($runImport = false) 421 { 422 $hadChanges = false; 423 424 $name = $this->get('source_name'); 425 Benchmark::measure("Starting with import $name"); 426 try { 427 $import = new Import($this); 428 $this->set('last_attempt', date('Y-m-d H:i:s')); 429 if ($import->providesChanges()) { 430 Benchmark::measure("Found changes for $name"); 431 $hadChanges = true; 432 $this->set('import_state', 'pending-changes'); 433 434 if ($runImport && $import->run()) { 435 Benchmark::measure("Import succeeded for $name"); 436 $this->set('import_state', 'in-sync'); 437 } 438 } else { 439 $this->set('import_state', 'in-sync'); 440 } 441 442 $this->set('last_error_message', null); 443 } catch (Exception $e) { 444 $this->set('import_state', 'failing'); 445 Benchmark::measure("Import failed for $name"); 446 $this->set('last_error_message', $e->getMessage()); 447 } 448 449 if ($this->hasBeenModified()) { 450 $this->store(); 451 } 452 453 return $hadChanges; 454 } 455 456 /** 457 * @return bool 458 * @throws DuplicateKeyException 459 */ 460 public function runImport() 461 { 462 return $this->checkForChanges(true); 463 } 464} 465