bulkchange.class.inc.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <?php
  2. /**
  3. * BulkChange
  4. * Interpret a given data set and update the DB accordingly (fake mode avail.)
  5. *
  6. * @package iTopORM
  7. * @author Romain Quetiez <romainquetiez@yahoo.fr>
  8. * @author Denis Flaven <denisflave@free.fr>
  9. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  10. * @link www.itop.com
  11. * @since 1.0
  12. * @version 1.1.1.1 $
  13. */
  14. class BulkChangeException extends CoreException
  15. {
  16. }
  17. /**
  18. * CellChangeSpec
  19. * A series of classes, keeping the information about a given cell: could it be changed or not (and why)?
  20. *
  21. * @package iTopORM
  22. * @author Romain Quetiez <romainquetiez@yahoo.fr>
  23. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  24. * @link www.itop.com
  25. * @since 1.0
  26. * @version $itopversion$
  27. */
  28. abstract class CellChangeSpec
  29. {
  30. protected $m_proposedValue;
  31. public function __construct($proposedValue)
  32. {
  33. $this->m_proposedValue = $proposedValue;
  34. }
  35. static protected function ValueAsHtml($value)
  36. {
  37. if (MetaModel::IsValidObject($value))
  38. {
  39. return $value->GetHyperLink();
  40. }
  41. else
  42. {
  43. return htmlentities($value);
  44. }
  45. }
  46. public function GetValue($bHtml = false)
  47. {
  48. if ($bHtml)
  49. {
  50. return self::ValueAsHtml($this->m_proposedValue);
  51. }
  52. else
  53. {
  54. return $this->m_proposedValue;
  55. }
  56. }
  57. abstract public function GetDescription($bHtml = false);
  58. }
  59. class CellChangeSpec_Void extends CellChangeSpec
  60. {
  61. public function GetDescription($bHtml = false)
  62. {
  63. return $this->GetValue($bHtml);
  64. }
  65. }
  66. class CellChangeSpec_Unchanged extends CellChangeSpec
  67. {
  68. public function GetDescription($bHtml = false)
  69. {
  70. return $this->GetValue($bHtml)." (unchanged)";
  71. }
  72. }
  73. class CellChangeSpec_Init extends CellChangeSpec
  74. {
  75. public function GetDescription($bHtml = false)
  76. {
  77. return $this->GetValue($bHtml);
  78. }
  79. }
  80. class CellChangeSpec_Modify extends CellChangeSpec
  81. {
  82. protected $m_previousValue;
  83. public function __construct($proposedValue, $previousValue)
  84. {
  85. $this->m_previousValue = $previousValue;
  86. parent::__construct($proposedValue);
  87. }
  88. public function GetDescription($bHtml = false)
  89. {
  90. return $this->GetValue($bHtml)." (previous: ".self::ValueAsHtml($this->m_previousValue).")";
  91. }
  92. }
  93. class CellChangeSpec_Issue extends CellChangeSpec_Modify
  94. {
  95. protected $m_sReason;
  96. public function __construct($proposedValue, $previousValue, $sReason)
  97. {
  98. $this->m_sReason = $sReason;
  99. parent::__construct($proposedValue, $previousValue);
  100. }
  101. public function GetDescription($bHtml = false)
  102. {
  103. if (is_null($this->m_proposedValue))
  104. {
  105. return 'Could not be changed - reason: '.$this->m_sReason;
  106. }
  107. return 'Could not be changed to "'.$this->GetValue($bHtml).'" - reason: '.$this->m_sReason.' (previous: '.$this->m_previousValue.')';
  108. }
  109. }
  110. class CellChangeSpec_Ambiguous extends CellChangeSpec_Modify
  111. {
  112. protected $m_iCount;
  113. protected $m_sOql;
  114. public function __construct($previousValue, $iCount, $sOql)
  115. {
  116. $this->m_iCount = $iCount;
  117. $this->m_sQuery = $sOql;
  118. parent::__construct(null, $previousValue);
  119. }
  120. public function GetDescription($bHtml = false)
  121. {
  122. if ($bHtml)
  123. {
  124. $sCount = '<a href="'.$this->m_sQuery.'">'.$this->m_iCount.'</a>';
  125. }
  126. else
  127. {
  128. $sCount = $this->m_iCount;
  129. }
  130. return "Ambiguous: found $sCount objects";
  131. }
  132. }
  133. /**
  134. * RowStatus
  135. * A series of classes, keeping the information about a given row: could it be changed or not (and why)?
  136. *
  137. * @package iTopORM
  138. * @author Romain Quetiez <romainquetiez@yahoo.fr>
  139. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  140. * @link www.itop.com
  141. * @since 1.0
  142. * @version $itopversion$
  143. */
  144. abstract class RowStatus
  145. {
  146. public function __construct()
  147. {
  148. }
  149. abstract public function GetDescription($bHtml = false);
  150. }
  151. class RowStatus_NoChange extends RowStatus
  152. {
  153. public function GetDescription($bHtml = false)
  154. {
  155. return "unchanged";
  156. }
  157. }
  158. class RowStatus_NewObj extends RowStatus
  159. {
  160. protected $m_iObjKey;
  161. public function __construct($sClass = '', $iObjKey = null)
  162. {
  163. $this->m_iObjKey = $iObjKey;
  164. $this->m_sClass = $sClass;
  165. }
  166. public function GetDescription($bHtml = false)
  167. {
  168. if (is_null($this->m_iObjKey))
  169. {
  170. return "Create";
  171. }
  172. else
  173. {
  174. if (!empty($this->m_sClass))
  175. {
  176. $oObj = MetaModel::GetObject($this->m_sClass, $this->m_iObjKey);
  177. return 'Created '.$oObj->GetHyperLink();
  178. }
  179. else
  180. {
  181. return 'Created (id: '.$this->m_iObjKey.')';
  182. }
  183. }
  184. }
  185. }
  186. class RowStatus_Modify extends RowStatus
  187. {
  188. protected $m_iChanged;
  189. public function __construct($iChanged)
  190. {
  191. $this->m_iChanged = $iChanged;
  192. }
  193. public function GetDescription($bHtml = false)
  194. {
  195. return "update ".$this->m_iChanged." cols";
  196. }
  197. }
  198. class RowStatus_Issue extends RowStatus
  199. {
  200. protected $m_sReason;
  201. public function __construct($sReason)
  202. {
  203. $this->m_sReason = $sReason;
  204. }
  205. public function GetDescription($bHtml = false)
  206. {
  207. return 'Skipped - reason:'.$this->m_sReason;
  208. }
  209. }
  210. /**
  211. ** BulkChange *
  212. ** @package iTopORM
  213. ** @author Romain Quetiez <romainquetiez@yahoo.fr>
  214. ** @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  215. ** @link www.itop.com
  216. ** @since 1.0
  217. ** @version $itopversion$ */
  218. class BulkChange
  219. {
  220. protected $m_sClass;
  221. protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
  222. // #@# todo: rename the variables to sColIndex
  223. protected $m_aAttList; // attcode => iCol
  224. protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
  225. protected $m_aReconcilKeys;// attcode (attcode = 'id' for the pkey)
  226. public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys)
  227. {
  228. $this->m_sClass = $sClass;
  229. $this->m_aData = $aData;
  230. $this->m_aAttList = $aAttList;
  231. $this->m_aReconcilKeys = $aReconcilKeys;
  232. $this->m_aExtKeys = $aExtKeys;
  233. }
  234. static protected function MakeSpecObject($sClass, $iId)
  235. {
  236. try
  237. {
  238. $oObj = MetaModel::GetObject($sClass, $iId);
  239. }
  240. catch(CoreException $e)
  241. {
  242. // in case an ext key is 0 (which is currently acceptable)
  243. return $iId;
  244. }
  245. return $oObj;
  246. }
  247. protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
  248. {
  249. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  250. $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
  251. foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
  252. {
  253. // The foreign attribute is one of our reconciliation key
  254. $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
  255. $aResults["col$iCol"] = new CellChangeSpec_Void($aRowData[$iCol]);
  256. }
  257. $oExtObjects = new CMDBObjectSet($oReconFilter);
  258. $aKeys = $oExtObjects->ToArray();
  259. return $aKeys;
  260. }
  261. protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors)
  262. {
  263. $aResults = array();
  264. $aErrors = array();
  265. // External keys reconciliation
  266. //
  267. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  268. {
  269. // Skip external keys used for the reconciliation process
  270. if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
  271. $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
  272. $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
  273. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  274. {
  275. // The foreign attribute is one of our reconciliation key
  276. $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
  277. $aResults["col$iCol"] = new CellChangeSpec_Void($aRowData[$iCol]);
  278. }
  279. $oExtObjects = new CMDBObjectSet($oReconFilter);
  280. switch($oExtObjects->Count())
  281. {
  282. case 0:
  283. if ($oExtKey->IsNullAllowed())
  284. {
  285. $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue());
  286. }
  287. else
  288. {
  289. $aErrors[$sAttCode] = "Object not found";
  290. $aResults[$sAttCode]= new CellChangeSpec_Issue(null, $oTargetObj->Get($sAttCode), 'Object not found - check the spelling (no space before/after)');
  291. }
  292. break;
  293. case 1:
  294. // Do change the external key attribute
  295. $oForeignObj = $oExtObjects->Fetch();
  296. $oTargetObj->Set($sAttCode, $oForeignObj->GetKey());
  297. break;
  298. default:
  299. $aErrors[$sAttCode] = "Found ".$oExtObjects->Count()." matches";
  300. $previousValue = self::MakeSpecObject($oExtKey->GetTargetClass(), $oTargetObj->Get($sAttCode));
  301. $aResults[$sAttCode]= new CellChangeSpec_Ambiguous($previousValue, $oExtObjects->Count(), $oExtObjects->ToOql());
  302. }
  303. // Report
  304. if (!array_key_exists($sAttCode, $aResults))
  305. {
  306. $oForeignObj = $oTargetObj->Get($sAttCode);
  307. if (array_key_exists($sAttCode, $oTargetObj->ListChanges()))
  308. {
  309. if ($oTargetObj->IsNew())
  310. {
  311. $aResults[$sAttCode]= new CellChangeSpec_Init($oForeignObj);
  312. }
  313. else
  314. {
  315. $previousValue = self::MakeSpecObject($oExtKey->GetTargetClass(), $oTargetObj->GetOriginal($sAttCode));
  316. $aResults[$sAttCode]= new CellChangeSpec_Modify($oForeignObj, $previousValue);
  317. }
  318. }
  319. else
  320. {
  321. $aResults[$sAttCode]= new CellChangeSpec_Unchanged($oForeignObj);
  322. }
  323. }
  324. }
  325. // Set the object attributes
  326. //
  327. foreach ($this->m_aAttList as $sAttCode => $iCol)
  328. {
  329. // skip the private key, if any
  330. if ($sAttCode == 'id') continue;
  331. if (!$oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]))
  332. {
  333. $aErrors[$sAttCode] = "Unexpected value";
  334. }
  335. else
  336. {
  337. $oTargetObj->Set($sAttCode, $aRowData[$iCol]);
  338. }
  339. }
  340. // Reporting on fields
  341. //
  342. $aChangedFields = $oTargetObj->ListChanges();
  343. foreach ($this->m_aAttList as $sAttCode => $iCol)
  344. {
  345. if ($sAttCode == 'id')
  346. {
  347. if ($aRowData[$iCol] == $oTargetObj->GetKey())
  348. {
  349. $aResults["col$iCol"]= new CellChangeSpec_Void($aRowData[$iCol]);
  350. }
  351. else
  352. {
  353. $aResults["col$iCol"]= new CellChangeSpec_Init($aRowData[$iCol]);
  354. }
  355. }
  356. if (isset($aErrors[$sAttCode]))
  357. {
  358. $aResults["col$iCol"]= new CellChangeSpec_Issue($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode), $aErrors[$sAttCode]);
  359. }
  360. elseif (array_key_exists($sAttCode, $aChangedFields))
  361. {
  362. if ($oTargetObj->IsNew())
  363. {
  364. $aResults["col$iCol"]= new CellChangeSpec_Init($oTargetObj->Get($sAttCode));
  365. }
  366. else
  367. {
  368. $aResults["col$iCol"]= new CellChangeSpec_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode));
  369. }
  370. }
  371. else
  372. {
  373. // By default... nothing happens
  374. $aResults["col$iCol"]= new CellChangeSpec_Void($aRowData[$iCol]);
  375. }
  376. }
  377. // Checks
  378. //
  379. if (!$oTargetObj->CheckConsistency())
  380. {
  381. $aErrors["GLOBAL"] = "Attributes not consistent with each others";
  382. }
  383. return $aResults;
  384. }
  385. protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null)
  386. {
  387. $oTargetObj = MetaModel::NewObject($this->m_sClass);
  388. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  389. if (count($aErrors) > 0)
  390. {
  391. $sErrors = implode(', ', $aErrors);
  392. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  393. return;
  394. }
  395. // Check that any external key will have a value proposed
  396. $aMissingKeys = array();
  397. foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey)
  398. {
  399. if (!$oExtKey->IsNullAllowed())
  400. {
  401. if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList))
  402. {
  403. $aMissingKeys[] = $oExtKey->GetLabel();
  404. }
  405. }
  406. }
  407. if (count($aMissingKeys) > 0)
  408. {
  409. $sMissingKeys = implode(', ', $aMissingKeys);
  410. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Could not be created, due to missing external key(s): $sMissingKeys");
  411. return;
  412. }
  413. // Optionaly record the results
  414. //
  415. if ($oChange)
  416. {
  417. $newID = $oTargetObj->DBInsertTrackedNoReload($oChange);
  418. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj($this->m_sClass, $newID);
  419. }
  420. else
  421. {
  422. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj();
  423. }
  424. }
  425. protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null)
  426. {
  427. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  428. // Reporting
  429. //
  430. if (count($aErrors) > 0)
  431. {
  432. $sErrors = implode(', ', $aErrors);
  433. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  434. return;
  435. }
  436. $aChangedFields = $oTargetObj->ListChanges();
  437. if (count($aChangedFields) > 0)
  438. {
  439. $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
  440. // Optionaly record the results
  441. //
  442. if ($oChange)
  443. {
  444. $oTargetObj->DBUpdateTracked($oChange);
  445. }
  446. }
  447. else
  448. {
  449. $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange();
  450. }
  451. }
  452. public function Process(CMDBChange $oChange = null)
  453. {
  454. // Note: $oChange can be null, in which case the aim is to check what would be done
  455. // Compute the results
  456. //
  457. $aResult = array();
  458. foreach($this->m_aData as $iRow => $aRowData)
  459. {
  460. $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass);
  461. $bSkipQuery = false;
  462. foreach($this->m_aReconcilKeys as $sAttCode)
  463. {
  464. $valuecondition = null;
  465. if (array_key_exists($sAttCode, $this->m_aExtKeys))
  466. {
  467. // The value has to be found or verified
  468. $aMatches = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
  469. if (count($aMatches) == 1)
  470. {
  471. $oRemoteObj = reset($aMatches); // first item
  472. $valuecondition = $oRemoteObj->GetKey();
  473. $aResult[$iRow][$sAttCode] = new CellChangeSpec_Void($oRemoteObj->GetKey());
  474. }
  475. elseif (count($aMatches) == 0)
  476. {
  477. $aResult[$iRow]["__RECONCILIATION__"] = "Could not find a match for external key '$sAttCode'";
  478. $aResult[$iRow][$sAttCode] = new CellChangeSpec_Issue(null, null, 'object not found');
  479. }
  480. else
  481. {
  482. $aResult[$iRow]["__RECONCILIATION__"] = "Ambiguous external key '$sAttCode'";
  483. $aResult[$iRow][$sAttCode] = new CellChangeSpec_Issue(null, null, 'found '.count($aMatches).' matches');
  484. }
  485. }
  486. else
  487. {
  488. // The value is given in the data row
  489. $iCol = $this->m_aAttList[$sAttCode];
  490. $valuecondition = $aRowData[$iCol];
  491. }
  492. if (is_null($valuecondition))
  493. {
  494. $bSkipQuery = true;
  495. }
  496. else
  497. {
  498. $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
  499. }
  500. }
  501. if ($bSkipQuery)
  502. {
  503. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile");
  504. }
  505. else
  506. {
  507. $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
  508. switch($oReconciliationSet->Count())
  509. {
  510. case 0:
  511. $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
  512. // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
  513. $aResult[$iRow]["__RECONCILIATION__"] = "Object not found";
  514. break;
  515. case 1:
  516. $oTargetObj = $oReconciliationSet->Fetch();
  517. $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
  518. $aResult[$iRow]["__RECONCILIATION__"] = "Found a match ".$oTargetObj->GetHyperLink();
  519. // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
  520. break;
  521. default:
  522. // Found several matches, ambiguous
  523. // Render "void" results on any column
  524. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  525. {
  526. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  527. {
  528. $aResult[$iRow]["col$iCol"] = new CellChangeSpec_Void($aRowData[$iCol]);
  529. }
  530. $aResult[$iRow][$sAttCode] = new CellChangeSpec_Void('n/a');
  531. }
  532. foreach ($this->m_aAttList as $sAttCode => $iCol)
  533. {
  534. $aResult[$iRow]["col$iCol"]= new CellChangeSpec_Void($aRowData[$iCol]);
  535. }
  536. $aResult[$iRow]["__RECONCILIATION__"] = "Found ".$oReconciliationSet->Count()." matches";
  537. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation");
  538. }
  539. }
  540. // Whatever happened, do report the reconciliation values
  541. foreach($this->m_aAttList as $iCol)
  542. {
  543. if (!array_key_exists($iCol, $aResult[$iRow]))
  544. {
  545. $aResult[$iRow]["col$iCol"] = new CellChangeSpec_Void($aRowData[$iCol]);
  546. }
  547. }
  548. foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts)
  549. {
  550. if (!array_key_exists($sAttCode, $aResult[$iRow]))
  551. {
  552. $aResult[$iRow][$sAttCode] = new CellChangeSpec_Void('n/a');
  553. foreach ($aForeignAtts as $sForeignAttCode => $iCol)
  554. {
  555. // The foreign attribute is one of our reconciliation key
  556. $aResult[$iRow]["col$iCol"] = new CellChangeSpec_Void($aRowData[$iCol]);
  557. }
  558. }
  559. }
  560. }
  561. return $aResult;
  562. }
  563. }
  564. ?>