1<?php 2// Copyright (C) 2010-2017 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19require_once('dbobjectiterator.php'); 20 21 22/** 23 * The value for an attribute representing a set of links between the host object and "remote" objects 24 * 25 * @package iTopORM 26 * @copyright Copyright (C) 2010-2017 Combodo SARL 27 * @license http://opensource.org/licenses/AGPL-3.0 28 */ 29 30class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator 31{ 32 protected $sHostClass; // subclass of DBObject 33 protected $sAttCode; // xxxxxx_list 34 protected $sClass; // class of the links 35 36 /** 37 * @var DBObjectSet 38 */ 39 protected $oOriginalSet; 40 41 /** 42 * @var DBObject[] array of iObjectId => DBObject 43 */ 44 protected $aOriginalObjects = null; 45 46 /** 47 * @var bool 48 */ 49 protected $bHasDelta = false; 50 51 /** 52 * Object from the original set, minus the removed objects 53 * @var DBObject[] array of iObjectId => DBObject 54 */ 55 protected $aPreserved = array(); 56 57 /** 58 * @var DBObject[] New items 59 */ 60 protected $aAdded = array(); 61 62 /** 63 * @var DBObject[] Modified items (could also be found in aPreserved) 64 */ 65 protected $aModified = array(); 66 67 /** 68 * @var int[] Removed items 69 */ 70 protected $aRemoved = array(); 71 72 /** 73 * @var int Position in the collection 74 */ 75 protected $iCursor = 0; 76 77 /** 78 * __toString magical function overload. 79 */ 80 public function __toString() 81 { 82 return ''; 83 } 84 85 /** 86 * ormLinkSet constructor. 87 * @param $sHostClass 88 * @param $sAttCode 89 * @param DBObjectSet|null $oOriginalSet 90 * @throws Exception 91 */ 92 public function __construct($sHostClass, $sAttCode, DBObjectSet $oOriginalSet = null) 93 { 94 $this->sHostClass = $sHostClass; 95 $this->sAttCode = $sAttCode; 96 $this->oOriginalSet = $oOriginalSet ? clone $oOriginalSet : null; 97 98 $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sAttCode); 99 if (!$oAttDef instanceof AttributeLinkedSet) 100 { 101 throw new Exception("ormLinkSet: $sAttCode is not a link set"); 102 } 103 $this->sClass = $oAttDef->GetLinkedClass(); 104 if ($oOriginalSet && ($oOriginalSet->GetClass() != $this->sClass)) 105 { 106 throw new Exception("ormLinkSet: wrong class for the original set, found {$oOriginalSet->GetClass()} while expecting {$oAttDef->GetLinkedClass()}"); 107 } 108 } 109 110 public function GetFilter() 111 { 112 return clone $this->oOriginalSet->GetFilter(); 113 } 114 115 /** 116 * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB 117 * 118 * @param hash $aAttToLoad Format: alias => array of attribute_codes 119 * 120 * @return void 121 */ 122 public function OptimizeColumnLoad($aAttToLoad) 123 { 124 $this->oOriginalSet->OptimizeColumnLoad($aAttToLoad); 125 } 126 127 /** 128 * @param DBObject $oLink 129 */ 130 public function AddItem(DBObject $oLink) 131 { 132 assert($oLink instanceof $this->sClass); 133 // No impact on the iteration algorithm 134 $iObjectId = $oLink->GetKey(); 135 $this->aAdded[$iObjectId] = $oLink; 136 $this->bHasDelta = true; 137 } 138 139 /** 140 * @param DBObject $oObject 141 * @param string $sClassAlias 142 * @deprecated Since iTop 2.4, use ormLinkset->AddItem() instead. 143 */ 144 public function AddObject(DBObject $oObject, $sClassAlias = '') 145 { 146 $this->AddItem($oObject); 147 } 148 149 /** 150 * @param $iObjectId 151 */ 152 public function RemoveItem($iObjectId) 153 { 154 if (array_key_exists($iObjectId, $this->aPreserved)) 155 { 156 unset($this->aPreserved[$iObjectId]); 157 $this->aRemoved[$iObjectId] = $iObjectId; 158 $this->bHasDelta = true; 159 } 160 else 161 { 162 if (array_key_exists($iObjectId, $this->aAdded)) 163 { 164 unset($this->aAdded[$iObjectId]); 165 } 166 } 167 } 168 169 /** 170 * @param DBObject $oLink 171 */ 172 public function ModifyItem(DBObject $oLink) 173 { 174 assert($oLink instanceof $this->sClass); 175 176 $iObjectId = $oLink->GetKey(); 177 if (array_key_exists($iObjectId, $this->aPreserved)) 178 { 179 unset($this->aPreserved[$iObjectId]); 180 $this->aModified[$iObjectId] = $oLink; 181 $this->bHasDelta = true; 182 } 183 } 184 185 protected function LoadOriginalIds() 186 { 187 if ($this->aOriginalObjects === null) 188 { 189 if ($this->oOriginalSet) 190 { 191 $this->aOriginalObjects = $this->GetArrayOfIndex(); 192 $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified) 193 foreach ($this->aRemoved as $iObjectId) 194 { 195 if (array_key_exists($iObjectId, $this->aPreserved)) 196 { 197 unset($this->aPreserved[$iObjectId]); 198 } 199 } 200 foreach ($this->aModified as $iObjectId => $oLink) 201 { 202 if (array_key_exists($iObjectId, $this->aPreserved)) 203 { 204 unset($this->aPreserved[$iObjectId]); 205 } 206 } 207 } 208 else 209 { 210 211 // Nothing to load 212 $this->aOriginalObjects = array(); 213 $this->aPreserved = array(); 214 } 215 } 216 } 217 218 /** 219 * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it. 220 * @return array 221 */ 222 protected function GetArrayOfIndex() 223 { 224 $aRet = array(); 225 $this->oOriginalSet->Rewind(); 226 $iRow = 0; 227 while ($oObject = $this->oOriginalSet->Fetch()) 228 { 229 $aRet[$oObject->GetKey()] = $iRow++; 230 } 231 return $aRet; 232 } 233 234 /** 235 * @param bool $bWithId 236 * @return array 237 * @deprecated Since iTop 2.4, use foreach($this as $oItem){} instead 238 */ 239 public function ToArray($bWithId = true) 240 { 241 $aRet = array(); 242 foreach($this as $oItem) 243 { 244 if ($bWithId) 245 { 246 $aRet[$oItem->GetKey()] = $oItem; 247 } 248 else 249 { 250 $aRet[] = $oItem; 251 } 252 } 253 return $aRet; 254 } 255 256 /** 257 * @param string $sAttCode 258 * @param bool $bWithId 259 * @return array 260 */ 261 public function GetColumnAsArray($sAttCode, $bWithId = true) 262 { 263 $aRet = array(); 264 foreach($this as $oItem) 265 { 266 if ($bWithId) 267 { 268 $aRet[$oItem->GetKey()] = $oItem->Get($sAttCode); 269 } 270 else 271 { 272 $aRet[] = $oItem->Get($sAttCode); 273 } 274 } 275 return $aRet; 276 } 277 278 /** 279 * The class of the objects of the collection (at least a common ancestor) 280 * 281 * @return string 282 */ 283 public function GetClass() 284 { 285 return $this->sClass; 286 } 287 288 /** 289 * The total number of objects in the collection 290 * 291 * @return int 292 */ 293 public function Count() 294 { 295 $this->LoadOriginalIds(); 296 $iRet = count($this->aPreserved) + count($this->aAdded) + count($this->aModified); 297 return $iRet; 298 } 299 300 /** 301 * Position the cursor to the given 0-based position 302 * 303 * @param $iPosition 304 * @throws Exception 305 * @internal param int $iRow 306 */ 307 public function Seek($iPosition) 308 { 309 $this->LoadOriginalIds(); 310 311 $iCount = $this->Count(); 312 if ($iPosition >= $iCount) 313 { 314 throw new Exception("Invalid position $iPosition: the link set is made of $iCount items."); 315 } 316 $this->rewind(); 317 for($iPos = 0 ; $iPos < $iPosition ; $iPos++) 318 { 319 $this->next(); 320 } 321 } 322 323 /** 324 * Fetch the object at the current position in the collection and move the cursor to the next position. 325 * 326 * @return DBObject|null The fetched object or null when at the end 327 */ 328 public function Fetch() 329 { 330 $this->LoadOriginalIds(); 331 332 $ret = $this->current(); 333 if ($ret === false) 334 { 335 $ret = null; 336 } 337 $this->next(); 338 return $ret; 339 } 340 341 /** 342 * Return the current element 343 * @link http://php.net/manual/en/iterator.current.php 344 * @return mixed Can return any type. 345 */ 346 public function current() 347 { 348 $this->LoadOriginalIds(); 349 350 $iPreservedCount = count($this->aPreserved); 351 if ($this->iCursor < $iPreservedCount) 352 { 353 $iRet = current($this->aPreserved); 354 $this->oOriginalSet->Seek($iRet); 355 $oRet = $this->oOriginalSet->Fetch(); 356 } 357 else 358 { 359 $iModifiedCount = count($this->aModified); 360 if($this->iCursor < $iPreservedCount + $iModifiedCount) 361 { 362 $oRet = current($this->aModified); 363 } 364 else 365 { 366 $oRet = current($this->aAdded); 367 } 368 } 369 return $oRet; 370 } 371 372 /** 373 * Move forward to next element 374 * @link http://php.net/manual/en/iterator.next.php 375 * @return void Any returned value is ignored. 376 */ 377 public function next() 378 { 379 $this->LoadOriginalIds(); 380 381 $iPreservedCount = count($this->aPreserved); 382 if ($this->iCursor < $iPreservedCount) 383 { 384 next($this->aPreserved); 385 } 386 else 387 { 388 $iModifiedCount = count($this->aModified); 389 if($this->iCursor < $iPreservedCount + $iModifiedCount) 390 { 391 next($this->aModified); 392 } 393 else 394 { 395 next($this->aAdded); 396 } 397 } 398 // Increment AFTER moving the internal cursors because when starting aModified / aAdded, we must leave it intact 399 $this->iCursor++; 400 } 401 402 /** 403 * Return the key of the current element 404 * @link http://php.net/manual/en/iterator.key.php 405 * @return mixed scalar on success, or null on failure. 406 */ 407 public function key() 408 { 409 return $this->iCursor; 410 } 411 412 /** 413 * Checks if current position is valid 414 * @link http://php.net/manual/en/iterator.valid.php 415 * @return boolean The return value will be casted to boolean and then evaluated. 416 * Returns true on success or false on failure. 417 */ 418 public function valid() 419 { 420 $this->LoadOriginalIds(); 421 422 $iCount = $this->Count(); 423 $bRet = ($this->iCursor < $iCount); 424 return $bRet; 425 } 426 427 /** 428 * Rewind the Iterator to the first element 429 * @link http://php.net/manual/en/iterator.rewind.php 430 * @return void Any returned value is ignored. 431 */ 432 public function rewind() 433 { 434 $this->LoadOriginalIds(); 435 436 $this->iCursor = 0; 437 reset($this->aPreserved); 438 reset($this->aAdded); 439 reset($this->aModified); 440 } 441 442 public function HasDelta() 443 { 444 return $this->bHasDelta; 445 } 446 447 /** 448 * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this. 449 * @param ormLinkSet $oFellow 450 * @return bool|null 451 * @throws Exception 452 */ 453 public function Equals(ormLinkSet $oFellow) 454 { 455 $bRet = null; 456 if ($this === $oFellow) 457 { 458 $bRet = true; 459 } 460 else 461 { 462 if ( ($this->oOriginalSet !== $oFellow->oOriginalSet) 463 && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) 464 { 465 throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope'); 466 } 467 if ($this->HasDelta()) 468 { 469 throw new Exception('ormLinkSet::Equals assumes that left link set had no delta'); 470 } 471 $bRet = !$oFellow->HasDelta(); 472 } 473 return $bRet; 474 } 475 476 public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow) 477 { 478 if ($oFellow === $this) 479 { 480 throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one'); 481 } 482 $bUpdateFromDelta = false; 483 if ($oFellow instanceof ormLinkSet) 484 { 485 if ( ($this->oOriginalSet === $oFellow->oOriginalSet) 486 || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) 487 { 488 $bUpdateFromDelta = true; 489 } 490 } 491 492 if ($bUpdateFromDelta) 493 { 494 // Same original set -> simply update the delta 495 $this->iCursor = 0; 496 $this->aAdded = $oFellow->aAdded; 497 $this->aRemoved = $oFellow->aRemoved; 498 $this->aModified = $oFellow->aModified; 499 $this->aPreserved = $oFellow->aPreserved; 500 $this->bHasDelta = $oFellow->bHasDelta; 501 } 502 else 503 { 504 // For backward compatibility reasons, let's rebuild a delta... 505 506 // Reset the delta 507 $this->iCursor = 0; 508 $this->aAdded = array(); 509 $this->aRemoved = array(); 510 $this->aModified = array(); 511 $this->aPreserved = ($this->aOriginalObjects === null) ? array() : $this->aOriginalObjects; 512 $this->bHasDelta = false; 513 514 /** @var AttributeLinkedSet $oAttDef */ 515 $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); 516 $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); 517 $sAdditionalKey = null; 518 if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) 519 { 520 $sAdditionalKey = $oAttDef->GetExtKeyToRemote(); 521 } 522 // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference) 523 $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey); 524 $aChanges = $oComparator->GetDifferences(); 525 foreach ($aChanges['added'] as $oLink) 526 { 527 $this->AddItem($oLink); 528 } 529 530 foreach ($aChanges['modified'] as $oLink) 531 { 532 $this->ModifyItem($oLink); 533 } 534 535 foreach ($aChanges['removed'] as $oLink) 536 { 537 $this->RemoveItem($oLink->GetKey()); 538 } 539 } 540 } 541 542 /** 543 * Get the list of all modified (added, modified and removed) links 544 * 545 * @return array of link objects 546 * @throws \Exception 547 */ 548 public function ListModifiedLinks() 549 { 550 $aAdded = $this->aAdded; 551 $aModified = $this->aModified; 552 $aRemoved = array(); 553 if (count($this->aRemoved) > 0) 554 { 555 $oSearch = new DBObjectSearch($this->sClass); 556 $oSearch->AddCondition('id', $this->aRemoved, 'IN'); 557 $oSet = new DBObjectSet($oSearch); 558 $aRemoved = $oSet->ToArray(); 559 } 560 return array_merge($aAdded, $aModified, $aRemoved); 561 } 562 563 /** 564 * @param DBObject $oHostObject 565 */ 566 public function DBWrite(DBObject $oHostObject) 567 { 568 /** @var AttributeLinkedSet $oAttDef */ 569 $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode); 570 $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); 571 $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a'; 572 573 $aCheckLinks = array(); 574 $aCheckRemote = array(); 575 foreach ($this->aAdded as $oLink) 576 { 577 if ($oLink->IsNew()) 578 { 579 if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) 580 { 581 //todo: faire un test qui passe dans cette branche ! 582 $aCheckRemote[] = $oLink->Get($sExtKeyToRemote); 583 } 584 } 585 else 586 { 587 //todo: faire un test qui passe dans cette branche ! 588 $aCheckLinks[] = $oLink->GetKey(); 589 } 590 } 591 foreach ($this->aRemoved as $iLinkId) 592 { 593 $aCheckLinks[] = $iLinkId; 594 } 595 foreach ($this->aModified as $iLinkId => $oLink) 596 { 597 $aCheckLinks[] = $oLink->GetKey(); 598 } 599 600 // Critical section : serialize any write access to these links 601 // 602 $oMtx = new iTopMutex('Write-'.$this->sClass); 603 $oMtx->Lock(); 604 605 // Check for the existing links 606 // 607 /** @var DBObject[] $aExistingLinks */ 608 $aExistingLinks = array(); 609 /** @var Int[] $aExistingRemote */ 610 $aExistingRemote = array(); 611 if (count($aCheckLinks) > 0) 612 { 613 $oSearch = new DBObjectSearch($this->sClass); 614 $oSearch->AddCondition('id', $aCheckLinks, 'IN'); 615 $oSet = new DBObjectSet($oSearch); 616 $aExistingLinks = $oSet->ToArray(); 617 } 618 619 // Check for the existing remote objects 620 // 621 if (count($aCheckRemote) > 0) 622 { 623 $oSearch = new DBObjectSearch($this->sClass); 624 $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '='); 625 $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN'); 626 $oSet = new DBObjectSet($oSearch); 627 $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote, true); 628 } 629 630 // Write the links according to the existing links 631 // 632 foreach ($this->aAdded as $oLink) 633 { 634 // Make sure that the objects in the set point to "this" 635 $oLink->Set($sExtKeyToMe, $oHostObject->GetKey()); 636 637 if ($oLink->IsNew()) 638 { 639 if (count($aCheckRemote) > 0) 640 { 641 $bIsDuplicate = false; 642 foreach($aExistingRemote as $sLinkKey => $sExtKey) 643 { 644 if ($sExtKey == $oLink->Get($sExtKeyToRemote)) 645 { 646 // Do not create a duplicate 647 // + In the case of a remove action followed by an add action 648 // of an existing link, 649 // the final state to consider is add action, 650 // so suppress the entry in the removed list. 651 if (array_key_exists($sLinkKey, $this->aRemoved)) 652 { 653 unset($this->aRemoved[$sLinkKey]); 654 } 655 $bIsDuplicate = true; 656 break; 657 } 658 } 659 if ($bIsDuplicate) 660 { 661 continue; 662 } 663 } 664 665 } 666 else 667 { 668 if (!array_key_exists($oLink->GetKey(), $aExistingLinks)) 669 { 670 $oLink->DBClone(); 671 } 672 } 673 $oLink->DBWrite(); 674 } 675 foreach ($this->aRemoved as $iLinkId) 676 { 677 if (array_key_exists($iLinkId, $aExistingLinks)) 678 { 679 $oLink = $aExistingLinks[$iLinkId]; 680 if ($oAttDef->IsIndirect()) 681 { 682 $oLink->DBDelete(); 683 } 684 else 685 { 686 $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe); 687 if ($oExtKeyToRemote->IsNullAllowed()) 688 { 689 if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey()) 690 { 691 // Detach the link object from this 692 $oLink->Set($sExtKeyToMe, 0); 693 $oLink->DBUpdate(); 694 } 695 } 696 else 697 { 698 $oLink->DBDelete(); 699 } 700 } 701 } 702 } 703 // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored 704 foreach ($this->aModified as $iLinkId => $oLink) 705 { 706 if (array_key_exists($oLink->GetKey(), $aExistingLinks)) 707 { 708 $oLink->DBUpdate(); 709 } 710 else 711 { 712 $oLink->DBClone(); 713 } 714 } 715 716 // End of the critical section 717 // 718 $oMtx->Unlock(); 719 } 720 721 public function ToDBObjectSet($bShowObsolete = true) 722 { 723 $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); 724 $oLinkSearch = $this->GetFilter(); 725 if ($oAttDef->IsIndirect()) 726 { 727 $sExtKeyToRemote = $oAttDef->GetExtKeyToRemote(); 728 $oLinkingAttDef = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToRemote); 729 $sTargetClass = $oLinkingAttDef->GetTargetClass(); 730 if (!$bShowObsolete && MetaModel::IsObsoletable($sTargetClass)) 731 { 732 $oNotObsolete = new BinaryExpression( 733 new FieldExpression('obsolescence_flag', $sTargetClass), 734 '=', 735 new ScalarExpression(0) 736 ); 737 $oNotObsoleteRemote = new DBObjectSearch($sTargetClass); 738 $oNotObsoleteRemote->AddConditionExpression($oNotObsolete); 739 $oLinkSearch->AddCondition_PointingTo($oNotObsoleteRemote, $sExtKeyToRemote); 740 } 741 } 742 $oLinkSet = new DBObjectSet($oLinkSearch); 743 $oLinkSet->SetShowObsoleteData($bShowObsolete); 744 if ($this->HasDelta()) 745 { 746 $oLinkSet->AddObjectArray($this->aAdded); 747 } 748 return $oLinkSet; 749 } 750}