123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- <?php
- // Copyright (C) 2010-2017 Combodo SARL
- //
- // This file is part of iTop.
- //
- // iTop is free software; you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // iTop is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with iTop. If not, see <http://www.gnu.org/licenses/>
- require_once('dbobjectiterator.php');
- /**
- * The value for an attribute representing a set of links between the host object and "remote" objects
- *
- * @package iTopORM
- * @copyright Copyright (C) 2010-2017 Combodo SARL
- * @license http://opensource.org/licenses/AGPL-3.0
- */
- class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
- {
- protected $sHostClass; // subclass of DBObject
- protected $sAttCode; // xxxxxx_list
- protected $sClass; // class of the links
- /**
- * @var DBObjectSet
- */
- protected $oOriginalSet;
- /**
- * @var DBObject[] array of iObjectId => DBObject
- */
- protected $aOriginalObjects = null;
- /**
- * @var bool
- */
- protected $bHasDelta = false;
- /**
- * Object from the original set, minus the removed objects
- * @var DBObject[] array of iObjectId => DBObject
- */
- protected $aPreserved = array();
- /**
- * @var DBObject[] New items
- */
- protected $aAdded = array();
- /**
- * @var DBObject[] Modified items (could also be found in aPreserved)
- */
- protected $aModified = array();
- /**
- * @var int[] Removed items
- */
- protected $aRemoved = array();
- /**
- * @var int Position in the collection
- */
- protected $iCursor = 0;
- /**
- * ormLinkSet constructor.
- * @param $sHostClass
- * @param $sAttCode
- * @param DBObjectSet|null $oOriginalSet
- * @throws Exception
- */
- public function __construct($sHostClass, $sAttCode, DBObjectSet $oOriginalSet = null)
- {
- $this->sHostClass = $sHostClass;
- $this->sAttCode = $sAttCode;
- $this->oOriginalSet = $oOriginalSet ? clone $oOriginalSet : null;
- $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sAttCode);
- if (!$oAttDef instanceof AttributeLinkedSet)
- {
- throw new Exception("ormLinkSet: $sAttCode is not a link set");
- }
- $this->sClass = $oAttDef->GetLinkedClass();
- if ($oOriginalSet && ($oOriginalSet->GetClass() != $this->sClass))
- {
- throw new Exception("ormLinkSet: wrong class for the original set, found {$oOriginalSet->GetClass()} while expecting {$oAttDef->GetLinkedClass()}");
- }
- }
- public function GetFilter()
- {
- return clone $this->oOriginalSet->GetFilter();
- }
- /**
- * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB
- *
- * @param hash $aAttToLoad Format: alias => array of attribute_codes
- *
- * @return void
- */
- public function OptimizeColumnLoad($aAttToLoad)
- {
- $this->oOriginalSet->OptimizeColumnLoad($aAttToLoad);
- }
- /**
- * @param DBObject $oLink
- */
- public function AddItem(DBObject $oLink)
- {
- assert($oLink instanceof $this->sClass);
- // No impact on the iteration algorithm
- $this->aAdded[] = $oLink;
- $this->bHasDelta = true;
- }
- /**
- * @param DBObject $oObject
- * @param string $sClassAlias
- * @deprecated Since iTop 2.4, use ormLinkset->AddItem() instead.
- */
- public function AddObject(DBObject $oObject, $sClassAlias = '')
- {
- $this->AddItem($oObject);
- }
- /**
- * @param $iObjectId
- */
- public function RemoveItem($iObjectId)
- {
- if (array_key_exists($iObjectId, $this->aPreserved))
- {
- unset($this->aPreserved[$iObjectId]);
- $this->aRemoved[$iObjectId] = $iObjectId;
- $this->bHasDelta = true;
- }
- }
- /**
- * @param DBObject $oLink
- */
- public function ModifyItem(DBObject $oLink)
- {
- assert($oLink instanceof $this->sClass);
- $iObjectId = $oLink->GetKey();
- if (array_key_exists($iObjectId, $this->aPreserved))
- {
- unset($this->aPreserved[$iObjectId]);
- $this->aModified[$iObjectId] = $oLink;
- $this->bHasDelta = true;
- }
- }
- protected function LoadOriginalIds()
- {
- if ($this->aOriginalObjects === null)
- {
- if ($this->oOriginalSet)
- {
- $this->aOriginalObjects = $this->GetArrayOfIndex();
- $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified)
- foreach ($this->aRemoved as $iObjectId)
- {
- if (array_key_exists($iObjectId, $this->aPreserved))
- {
- unset($this->aPreserved[$iObjectId]);
- }
- }
- foreach ($this->aModified as $iObjectId => $oLink)
- {
- if (array_key_exists($iObjectId, $this->aPreserved))
- {
- unset($this->aPreserved[$iObjectId]);
- }
- }
- }
- else
- {
- // Nothing to load
- $this->aOriginalObjects = array();
- $this->aPreserved = array();
- }
- }
- }
- /**
- * Note: After calling this method, the set cursor will be at the end of the set. You might want to rewind it.
- * @return array
- */
- protected function GetArrayOfIndex()
- {
- $aRet = array();
- $this->oOriginalSet->Rewind();
- $iRow = 0;
- while ($oObject = $this->oOriginalSet->Fetch())
- {
- $aRet[$oObject->GetKey()] = $iRow++;
- }
- return $aRet;
- }
- /**
- * @param bool $bWithId
- * @return array
- * @deprecated Since iTop 2.4, use foreach($this as $oItem){} instead
- */
- public function ToArray($bWithId = true)
- {
- $aRet = array();
- foreach($this as $oItem)
- {
- if ($bWithId)
- {
- $aRet[$oItem->GetKey()] = $oItem;
- }
- else
- {
- $aRet[] = $oItem;
- }
- }
- return $aRet;
- }
- /**
- * @param string $sAttCode
- * @param bool $bWithId
- * @return array
- */
- public function GetColumnAsArray($sAttCode, $bWithId = true)
- {
- $aRet = array();
- foreach($this as $oItem)
- {
- if ($bWithId)
- {
- $aRet[$oItem->GetKey()] = $oItem->Get($sAttCode);
- }
- else
- {
- $aRet[] = $oItem->Get($sAttCode);
- }
- }
- return $aRet;
- }
- /**
- * The class of the objects of the collection (at least a common ancestor)
- *
- * @return string
- */
- public function GetClass()
- {
- return $this->sClass;
- }
- /**
- * The total number of objects in the collection
- *
- * @return int
- */
- public function Count()
- {
- $this->LoadOriginalIds();
- $iRet = count($this->aPreserved) + count($this->aAdded) + count($this->aModified);
- return $iRet;
- }
- /**
- * Position the cursor to the given 0-based position
- *
- * @param $iPosition
- * @throws Exception
- * @internal param int $iRow
- */
- public function Seek($iPosition)
- {
- $this->LoadOriginalIds();
- $iCount = $this->Count();
- if ($iPosition >= $iCount)
- {
- throw new Exception("Invalid position $iPosition: the link set is made of $iCount items.");
- }
- $this->rewind();
- for($iPos = 0 ; $iPos < $iPosition ; $iPos++)
- {
- $this->next();
- }
- }
- /**
- * Fetch the object at the current position in the collection and move the cursor to the next position.
- *
- * @return DBObject|null The fetched object or null when at the end
- */
- public function Fetch()
- {
- $this->LoadOriginalIds();
- $ret = $this->current();
- if ($ret === false)
- {
- $ret = null;
- }
- $this->next();
- return $ret;
- }
- /**
- * Return the current element
- * @link http://php.net/manual/en/iterator.current.php
- * @return mixed Can return any type.
- */
- public function current()
- {
- $this->LoadOriginalIds();
- $iPreservedCount = count($this->aPreserved);
- if ($this->iCursor < $iPreservedCount)
- {
- $iRet = current($this->aPreserved);
- $this->oOriginalSet->Seek($iRet);
- $oRet = $this->oOriginalSet->Fetch();
- }
- else
- {
- $iModifiedCount = count($this->aModified);
- if($this->iCursor < $iPreservedCount + $iModifiedCount)
- {
- $oRet = current($this->aModified);
- }
- else
- {
- $oRet = current($this->aAdded);
- }
- }
- return $oRet;
- }
- /**
- * Move forward to next element
- * @link http://php.net/manual/en/iterator.next.php
- * @return void Any returned value is ignored.
- */
- public function next()
- {
- $this->LoadOriginalIds();
- $iPreservedCount = count($this->aPreserved);
- if ($this->iCursor < $iPreservedCount)
- {
- next($this->aPreserved);
- }
- else
- {
- $iModifiedCount = count($this->aModified);
- if($this->iCursor < $iPreservedCount + $iModifiedCount)
- {
- next($this->aModified);
- }
- else
- {
- next($this->aAdded);
- }
- }
- // Increment AFTER moving the internal cursors because when starting aModified / aAdded, we must leave it intact
- $this->iCursor++;
- }
- /**
- * Return the key of the current element
- * @link http://php.net/manual/en/iterator.key.php
- * @return mixed scalar on success, or null on failure.
- */
- public function key()
- {
- return $this->iCursor;
- }
- /**
- * Checks if current position is valid
- * @link http://php.net/manual/en/iterator.valid.php
- * @return boolean The return value will be casted to boolean and then evaluated.
- * Returns true on success or false on failure.
- */
- public function valid()
- {
- $this->LoadOriginalIds();
- $iCount = $this->Count();
- $bRet = ($this->iCursor < $iCount);
- return $bRet;
- }
- /**
- * Rewind the Iterator to the first element
- * @link http://php.net/manual/en/iterator.rewind.php
- * @return void Any returned value is ignored.
- */
- public function rewind()
- {
- $this->LoadOriginalIds();
- $this->iCursor = 0;
- reset($this->aPreserved);
- reset($this->aAdded);
- reset($this->aModified);
- }
- public function HasDelta()
- {
- return $this->bHasDelta;
- }
- /**
- * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this.
- * @param ormLinkSet $oFellow
- * @return bool|null
- * @throws Exception
- */
- public function Equals(ormLinkSet $oFellow)
- {
- $bRet = null;
- if ($this === $oFellow)
- {
- $bRet = true;
- }
- else
- {
- if ( ($this->oOriginalSet !== $oFellow->oOriginalSet)
- && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) )
- {
- throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope');
- }
- if ($this->HasDelta())
- {
- throw new Exception('ormLinkSet::Equals assumes that left link set had no delta');
- }
- $bRet = !$oFellow->HasDelta();
- }
- return $bRet;
- }
- public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow)
- {
- if ($oFellow === $this)
- {
- throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one');
- }
- $bUpdateFromDelta = false;
- if ($oFellow instanceof ormLinkSet)
- {
- if ( ($this->oOriginalSet === $oFellow->oOriginalSet)
- || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) )
- {
- $bUpdateFromDelta = true;
- }
- }
- if ($bUpdateFromDelta)
- {
- // Same original set -> simply update the delta
- $this->iCursor = 0;
- $this->aAdded = $oFellow->aAdded;
- $this->aRemoved = $oFellow->aRemoved;
- $this->aModified = $oFellow->aModified;
- $this->aPreserved = $oFellow->aPreserved;
- $this->bHasDelta = $oFellow->bHasDelta;
- }
- else
- {
- // For backward compatibility reasons, let's rebuild a delta...
- // Reset the delta
- $this->iCursor = 0;
- $this->aAdded = array();
- $this->aRemoved = array();
- $this->aModified = array();
- $this->aPreserved = ($this->aOriginalObjects === null) ? array() : $this->aOriginalObjects;
- $this->bHasDelta = false;
- /** @var AttributeLinkedSet $oAttDef */
- $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode);
- $sExtKeyToMe = $oAttDef->GetExtKeyToMe();
- $sAdditionalKey = null;
- if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed())
- {
- $sAdditionalKey = $oAttDef->GetExtKeyToRemote();
- }
- // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference)
- $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey);
- $aChanges = $oComparator->GetDifferences();
- foreach ($aChanges['added'] as $oLink)
- {
- $this->AddItem($oLink);
- }
- foreach ($aChanges['modified'] as $oLink)
- {
- $this->ModifyItem($oLink);
- }
- foreach ($aChanges['removed'] as $oLink)
- {
- $this->RemoveItem($oLink->GetKey());
- }
- }
- }
- /**
- * @param DBObject $oHostObject
- */
- public function DBWrite(DBObject $oHostObject)
- {
- /** @var AttributeLinkedSet $oAttDef */
- $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode);
- $sExtKeyToMe = $oAttDef->GetExtKeyToMe();
- $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a';
- $aCheckLinks = array();
- $aCheckRemote = array();
- foreach ($this->aAdded as $oLink)
- {
- if ($oLink->IsNew())
- {
- if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed())
- {
- //todo: faire un test qui passe dans cette branche !
- $aCheckRemote[] = $oLink->Get($sExtKeyToRemote);
- }
- }
- else
- {
- //todo: faire un test qui passe dans cette branche !
- $aCheckLinks[] = $oLink->GetKey();
- }
- }
- foreach ($this->aRemoved as $iLinkId)
- {
- $aCheckLinks[] = $iLinkId;
- }
- foreach ($this->aModified as $iLinkId => $oLink)
- {
- $aCheckLinks[] = $oLink->GetKey();
- }
- // Critical section : serialize any write access to these links
- //
- $oMtx = new iTopMutex('Write-'.$this->sClass);
- $oMtx->Lock();
- // Check for the existing links
- //
- /** @var DBObject[] $aExistingLinks */
- $aExistingLinks = array();
- /** @var Int[] $aExistingRemote */
- $aExistingRemote = array();
- if (count($aCheckLinks) > 0)
- {
- $oSearch = new DBObjectSearch($this->sClass);
- $oSearch->AddCondition('id', $aCheckLinks, 'IN');
- $oSet = new DBObjectSet($oSearch);
- $aExistingLinks = $oSet->ToArray();
- }
- // Check for the existing remote objects
- //
- if (count($aCheckRemote) > 0)
- {
- $oSearch = new DBObjectSearch($this->sClass);
- $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '=');
- $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN');
- $oSet = new DBObjectSet($oSearch);
- $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote);
- }
- // Write the links according to the existing links
- //
- foreach ($this->aAdded as $oLink)
- {
- // Make sure that the objects in the set point to "this"
- $oLink->Set($sExtKeyToMe, $oHostObject->GetKey());
- if ($oLink->IsNew())
- {
- if (count($aCheckRemote) > 0)
- {
- if (in_array($oLink->Get($sExtKeyToRemote), $aExistingRemote))
- {
- // Do not create a duplicate
- continue;
- }
- }
- }
- else
- {
- if (!array_key_exists($oLink->GetKey(), $aExistingLinks))
- {
- $oLink->DBClone();
- }
- }
- $oLink->DBWrite();
- }
- foreach ($this->aRemoved as $iLinkId)
- {
- if (array_key_exists($iLinkId, $aExistingLinks))
- {
- $oLink = $aExistingLinks[$iLinkId];
- if ($oAttDef->IsIndirect())
- {
- $oLink->DBDelete();
- }
- else
- {
- $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe);
- if ($oExtKeyToRemote->IsNullAllowed())
- {
- if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey())
- {
- // Detach the link object from this
- $oLink->Set($sExtKeyToMe, 0);
- $oLink->DBUpdate();
- }
- }
- else
- {
- $oLink->DBDelete();
- }
- }
- }
- }
- // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored
- foreach ($this->aModified as $iLinkId => $oLink)
- {
- if (array_key_exists($oLink->GetKey(), $aExistingLinks))
- {
- $oLink->DBUpdate();
- }
- else
- {
- $oLink->DBClone();
- }
- }
- // End of the critical section
- //
- $oMtx->Unlock();
- }
- }
|