bulkchange.class.inc.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. <?php
  2. // Copyright (C) 2010 Combodo SARL
  3. //
  4. // This program is free software; you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation; version 3 of the License.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. /**
  17. * Bulk change facility (common to interactive and batch usages)
  18. *
  19. * @author Erwan Taloc <erwan.taloc@combodo.com>
  20. * @author Romain Quetiez <romain.quetiez@combodo.com>
  21. * @author Denis Flaven <denis.flaven@combodo.com>
  22. * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL
  23. */
  24. /**
  25. * BulkChange
  26. * Interpret a given data set and update the DB accordingly (fake mode avail.)
  27. *
  28. * @package iTopORM
  29. */
  30. class BulkChangeException extends CoreException
  31. {
  32. }
  33. /**
  34. * CellChangeSpec
  35. * A series of classes, keeping the information about a given cell: could it be changed or not (and why)?
  36. *
  37. * @package iTopORM
  38. */
  39. abstract class CellChangeSpec
  40. {
  41. protected $m_proposedValue;
  42. protected $m_sOql; // in case of ambiguity
  43. public function __construct($proposedValue, $sOql = '')
  44. {
  45. $this->m_proposedValue = $proposedValue;
  46. $this->m_sOql = $sOql;
  47. }
  48. public function GetPureValue()
  49. {
  50. // Todo - distinguish both values
  51. return $this->m_proposedValue;
  52. }
  53. public function GetDisplayableValue()
  54. {
  55. return $this->m_proposedValue;
  56. }
  57. public function GetOql()
  58. {
  59. return $this->m_sOql;
  60. }
  61. abstract public function GetDescription();
  62. }
  63. class CellStatus_Void extends CellChangeSpec
  64. {
  65. public function GetDescription()
  66. {
  67. return '';
  68. }
  69. }
  70. class CellStatus_Modify extends CellChangeSpec
  71. {
  72. protected $m_previousValue;
  73. public function __construct($proposedValue, $previousValue)
  74. {
  75. $this->m_previousValue = $previousValue;
  76. parent::__construct($proposedValue);
  77. }
  78. public function GetDescription()
  79. {
  80. return 'Modified';
  81. }
  82. //public function GetPreviousValue()
  83. //{
  84. // return $this->m_previousValue;
  85. //}
  86. }
  87. class CellStatus_Issue extends CellStatus_Modify
  88. {
  89. protected $m_sReason;
  90. public function __construct($proposedValue, $previousValue, $sReason)
  91. {
  92. $this->m_sReason = $sReason;
  93. parent::__construct($proposedValue, $previousValue);
  94. }
  95. public function GetDescription()
  96. {
  97. if (is_null($this->m_proposedValue))
  98. {
  99. return 'Could not be changed - reason: '.$this->m_sReason;
  100. }
  101. return 'Could not be changed to '.$this->m_proposedValue.' - reason: '.$this->m_sReason;
  102. }
  103. }
  104. class CellStatus_SearchIssue extends CellStatus_Issue
  105. {
  106. public function __construct()
  107. {
  108. parent::__construct(null, null, null);
  109. }
  110. public function GetDescription()
  111. {
  112. return 'No match';
  113. }
  114. }
  115. class CellStatus_NullIssue extends CellStatus_Issue
  116. {
  117. public function __construct()
  118. {
  119. parent::__construct(null, null, null);
  120. }
  121. public function GetDescription()
  122. {
  123. return 'Missing mandatory value';
  124. }
  125. }
  126. class CellStatus_Ambiguous extends CellStatus_Issue
  127. {
  128. protected $m_iCount;
  129. public function __construct($previousValue, $iCount, $sOql)
  130. {
  131. $this->m_iCount = $iCount;
  132. $this->m_sQuery = $sOql;
  133. parent::__construct(null, $previousValue, '');
  134. }
  135. public function GetDescription()
  136. {
  137. $sCount = $this->m_iCount;
  138. return "Ambiguous: found $sCount objects";
  139. }
  140. }
  141. /**
  142. * RowStatus
  143. * A series of classes, keeping the information about a given row: could it be changed or not (and why)?
  144. *
  145. * @package iTopORM
  146. */
  147. abstract class RowStatus
  148. {
  149. public function __construct()
  150. {
  151. }
  152. abstract public function GetDescription();
  153. }
  154. class RowStatus_NoChange extends RowStatus
  155. {
  156. public function GetDescription()
  157. {
  158. return "unchanged";
  159. }
  160. }
  161. class RowStatus_NewObj extends RowStatus
  162. {
  163. public function GetDescription()
  164. {
  165. return "created";
  166. }
  167. }
  168. class RowStatus_Modify extends RowStatus
  169. {
  170. protected $m_iChanged;
  171. public function __construct($iChanged)
  172. {
  173. $this->m_iChanged = $iChanged;
  174. }
  175. public function GetDescription()
  176. {
  177. return "updated ".$this->m_iChanged." cols";
  178. }
  179. }
  180. class RowStatus_Disappeared extends RowStatus_Modify
  181. {
  182. public function GetDescription()
  183. {
  184. return "disappeared, changed ".$this->m_iChanged." cols";
  185. }
  186. }
  187. class RowStatus_Issue extends RowStatus
  188. {
  189. protected $m_sReason;
  190. public function __construct($sReason)
  191. {
  192. $this->m_sReason = $sReason;
  193. }
  194. public function GetDescription()
  195. {
  196. return 'Issue: '.$this->m_sReason;
  197. }
  198. }
  199. /**
  200. * BulkChange
  201. *
  202. * @package iTopORM
  203. */
  204. class BulkChange
  205. {
  206. protected $m_sClass;
  207. protected $m_aData; // Note: hereafter, iCol maybe actually be any acceptable key (string)
  208. // #@# todo: rename the variables to sColIndex
  209. protected $m_aAttList; // attcode => iCol
  210. protected $m_aExtKeys; // aExtKeys[sExtKeyAttCode][sExtReconcKeyAttCode] = iCol;
  211. protected $m_aReconcilKeys; // attcode (attcode = 'id' for the pkey)
  212. protected $m_sSynchroScope; // OQL - if specified, then the missing items will be reported
  213. protected $m_aOnDisappear; // array of attcode => value, values to be set when an object gets out of scope (ignored if no scope has been defined)
  214. public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null)
  215. {
  216. $this->m_sClass = $sClass;
  217. $this->m_aData = $aData;
  218. $this->m_aAttList = $aAttList;
  219. $this->m_aReconcilKeys = $aReconcilKeys;
  220. $this->m_aExtKeys = $aExtKeys;
  221. $this->m_sSynchroScope = $sSynchroScope;
  222. $this->m_aOnDisappear = $aOnDisappear;
  223. }
  224. protected $m_bReportHtml = false;
  225. protected $m_sReportCsvSep = ',';
  226. protected $m_sReportCsvDelimiter = '"';
  227. public function SetReportHtml()
  228. {
  229. $this->m_bReportHtml = true;
  230. }
  231. public function SetReportCsv($sSeparator = ',', $sDelimiter = '"')
  232. {
  233. $this->m_bReportHtml = false;
  234. $this->m_sReportCsvSep = $sSeparator;
  235. $this->m_sReportCsvDelimiter = $sDelimiter;
  236. }
  237. protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
  238. {
  239. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  240. $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
  241. foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
  242. {
  243. // The foreign attribute is one of our reconciliation key
  244. $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
  245. $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
  246. }
  247. $oExtObjects = new CMDBObjectSet($oReconFilter);
  248. $aKeys = $oExtObjects->ToArray();
  249. return array($oReconFilter->ToOql(), $aKeys);
  250. }
  251. // Returns true if the CSV data specifies that the external key must be left undefined
  252. protected function IsNullExternalKeySpec($aRowData, $sAttCode)
  253. {
  254. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  255. foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
  256. {
  257. // The foreign attribute is one of our reconciliation key
  258. if (strlen($aRowData[$iCol]) > 0)
  259. {
  260. return false;
  261. }
  262. }
  263. return true;
  264. }
  265. protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors)
  266. {
  267. $aResults = array();
  268. $aErrors = array();
  269. // External keys reconciliation
  270. //
  271. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  272. {
  273. // Skip external keys used for the reconciliation process
  274. // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
  275. $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
  276. if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
  277. {
  278. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  279. {
  280. $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
  281. }
  282. if ($oExtKey->IsNullAllowed())
  283. {
  284. $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue());
  285. $aResults[$sAttCode]= new CellStatus_Void($oExtKey->GetNullValue());
  286. }
  287. else
  288. {
  289. $aErrors[$sAttCode] = "Null not allowed";
  290. $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), 'Null not allowed');
  291. }
  292. }
  293. else
  294. {
  295. $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
  296. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  297. {
  298. // The foreign attribute is one of our reconciliation key
  299. $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
  300. $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
  301. }
  302. $oExtObjects = new CMDBObjectSet($oReconFilter);
  303. switch($oExtObjects->Count())
  304. {
  305. case 0:
  306. $aErrors[$sAttCode] = "Object not found";
  307. $aResults[$sAttCode]= new CellStatus_SearchIssue();
  308. break;
  309. case 1:
  310. // Do change the external key attribute
  311. $oForeignObj = $oExtObjects->Fetch();
  312. $oTargetObj->Set($sAttCode, $oForeignObj->GetKey());
  313. break;
  314. default:
  315. $aErrors[$sAttCode] = "Found ".$oExtObjects->Count()." matches";
  316. $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $oExtObjects->Count(), $oReconFilter->ToOql());
  317. }
  318. }
  319. // Report
  320. if (!array_key_exists($sAttCode, $aResults))
  321. {
  322. $iForeignObj = $oTargetObj->Get($sAttCode);
  323. if (array_key_exists($sAttCode, $oTargetObj->ListChanges()))
  324. {
  325. if ($oTargetObj->IsNew())
  326. {
  327. $aResults[$sAttCode]= new CellStatus_Void($iForeignObj);
  328. }
  329. else
  330. {
  331. $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
  332. }
  333. }
  334. else
  335. {
  336. $aResults[$sAttCode]= new CellStatus_Void($iForeignObj);
  337. }
  338. }
  339. }
  340. // Set the object attributes
  341. //
  342. foreach ($this->m_aAttList as $sAttCode => $iCol)
  343. {
  344. // skip the private key, if any
  345. if ($sAttCode == 'id') continue;
  346. $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  347. if ($oAttDef->IsLinkSet())
  348. {
  349. try
  350. {
  351. $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol]);
  352. $oTargetObj->Set($sAttCode, $oSet);
  353. }
  354. catch(CoreException $e)
  355. {
  356. $aErrors[$sAttCode] = "Failed to process input: ".$e->getMessage();
  357. }
  358. }
  359. else
  360. {
  361. $res = $oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]);
  362. if ($res === true)
  363. {
  364. $oTargetObj->Set($sAttCode, $aRowData[$iCol]);
  365. }
  366. else
  367. {
  368. // $res is a string with the error description
  369. $aErrors[$sAttCode] = "Unexpected value for attribute '$sAttCode': $res";
  370. }
  371. }
  372. }
  373. // Reporting on fields
  374. //
  375. $aChangedFields = $oTargetObj->ListChanges();
  376. foreach ($this->m_aAttList as $sAttCode => $iCol)
  377. {
  378. if ($sAttCode == 'id')
  379. {
  380. $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
  381. }
  382. if ($this->m_bReportHtml)
  383. {
  384. $sCurValue = $oTargetObj->GetAsHTML($sAttCode);
  385. $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode);
  386. }
  387. else
  388. {
  389. $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
  390. $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
  391. }
  392. if (isset($aErrors[$sAttCode]))
  393. {
  394. $aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]);
  395. }
  396. elseif (array_key_exists($sAttCode, $aChangedFields))
  397. {
  398. if ($oTargetObj->IsNew())
  399. {
  400. $aResults[$iCol]= new CellStatus_Void($sCurValue);
  401. }
  402. else
  403. {
  404. $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue);
  405. }
  406. }
  407. else
  408. {
  409. // By default... nothing happens
  410. $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
  411. }
  412. }
  413. // Checks
  414. //
  415. $res = $oTargetObj->CheckConsistency();
  416. if ($res !== true)
  417. {
  418. // $res contains the error description
  419. $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res";
  420. }
  421. return $aResults;
  422. }
  423. protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
  424. {
  425. $aResults = array();
  426. $aErrors = array();
  427. // External keys
  428. //
  429. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  430. {
  431. //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
  432. $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode));
  433. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  434. {
  435. $aResults[$iCol] = new CellStatus_Void('?');
  436. }
  437. }
  438. // Update attributes
  439. //
  440. foreach($this->m_aOnDisappear as $sAttCode => $value)
  441. {
  442. if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode))
  443. {
  444. throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode));
  445. }
  446. $oTargetObj->Set($sAttCode, $value);
  447. if (!array_key_exists($sAttCode, $this->m_aAttList))
  448. {
  449. // #@# will be out of the reporting... (counted anyway)
  450. }
  451. }
  452. // Reporting on fields
  453. //
  454. $aChangedFields = $oTargetObj->ListChanges();
  455. foreach ($this->m_aAttList as $sAttCode => $iCol)
  456. {
  457. if ($sAttCode == 'id')
  458. {
  459. $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey());
  460. }
  461. if (array_key_exists($sAttCode, $aChangedFields))
  462. {
  463. $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode));
  464. }
  465. else
  466. {
  467. // By default... nothing happens
  468. $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode));
  469. }
  470. }
  471. // Checks
  472. //
  473. $res = $oTargetObj->CheckConsistency();
  474. if ($res !== true)
  475. {
  476. // $res contains the error description
  477. $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res";
  478. }
  479. return $aResults;
  480. }
  481. protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null)
  482. {
  483. $oTargetObj = MetaModel::NewObject($this->m_sClass);
  484. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  485. if (count($aErrors) > 0)
  486. {
  487. $sErrors = implode(', ', $aErrors);
  488. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  489. return $oTargetObj;
  490. }
  491. // Check that any external key will have a value proposed
  492. $aMissingKeys = array();
  493. foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey)
  494. {
  495. if (!$oExtKey->IsNullAllowed())
  496. {
  497. if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList))
  498. {
  499. $aMissingKeys[] = $oExtKey->GetLabel();
  500. }
  501. }
  502. }
  503. if (count($aMissingKeys) > 0)
  504. {
  505. $sMissingKeys = implode(', ', $aMissingKeys);
  506. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Could not be created, due to missing external key(s): $sMissingKeys");
  507. return $oTargetObj;
  508. }
  509. // Optionaly record the results
  510. //
  511. if ($oChange)
  512. {
  513. $newID = $oTargetObj->DBInsertTrackedNoReload($oChange);
  514. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj($this->m_sClass, $newID);
  515. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  516. $aResult[$iRow]["id"] = new CellStatus_Void($newID);
  517. }
  518. else
  519. {
  520. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj();
  521. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  522. $aResult[$iRow]["id"] = new CellStatus_Void(0);
  523. }
  524. return $oTargetObj;
  525. }
  526. protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null)
  527. {
  528. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  529. // Reporting
  530. //
  531. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  532. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  533. if (count($aErrors) > 0)
  534. {
  535. $sErrors = implode(', ', $aErrors);
  536. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  537. return;
  538. }
  539. $aChangedFields = $oTargetObj->ListChanges();
  540. if (count($aChangedFields) > 0)
  541. {
  542. $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
  543. // Optionaly record the results
  544. //
  545. if ($oChange)
  546. {
  547. $oTargetObj->DBUpdateTracked($oChange);
  548. }
  549. }
  550. else
  551. {
  552. $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange();
  553. }
  554. }
  555. protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null)
  556. {
  557. $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors);
  558. // Reporting
  559. //
  560. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  561. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  562. if (count($aErrors) > 0)
  563. {
  564. $sErrors = implode(', ', $aErrors);
  565. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  566. return;
  567. }
  568. $aChangedFields = $oTargetObj->ListChanges();
  569. if (count($aChangedFields) > 0)
  570. {
  571. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields));
  572. // Optionaly record the results
  573. //
  574. if ($oChange)
  575. {
  576. $oTargetObj->DBUpdateTracked($oChange);
  577. }
  578. }
  579. else
  580. {
  581. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
  582. }
  583. }
  584. public function Process(CMDBChange $oChange = null)
  585. {
  586. // Note: $oChange can be null, in which case the aim is to check what would be done
  587. // Debug...
  588. //
  589. if (false)
  590. {
  591. echo "<pre>\n";
  592. echo "Attributes:\n";
  593. print_r($this->m_aAttList);
  594. echo "ExtKeys:\n";
  595. print_r($this->m_aExtKeys);
  596. echo "Reconciliation:\n";
  597. print_r($this->m_aReconcilKeys);
  598. echo "Synchro scope:\n";
  599. print_r($this->m_sSynchroScope);
  600. echo "Synchro changes:\n";
  601. print_r($this->m_aOnDisappear);
  602. //echo "Data:\n";
  603. //print_r($this->m_aData);
  604. echo "</pre>\n";
  605. exit;
  606. }
  607. // Compute the results
  608. //
  609. if (!is_null($this->m_sSynchroScope))
  610. {
  611. $aVisited = array();
  612. }
  613. $aResult = array();
  614. foreach($this->m_aData as $iRow => $aRowData)
  615. {
  616. $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass);
  617. $bSkipQuery = false;
  618. foreach($this->m_aReconcilKeys as $sAttCode)
  619. {
  620. $valuecondition = null;
  621. if (array_key_exists($sAttCode, $this->m_aExtKeys))
  622. {
  623. if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
  624. {
  625. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  626. if ($oExtKey->IsNullAllowed())
  627. {
  628. $valuecondition = $oExtKey->GetNullValue();
  629. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
  630. }
  631. else
  632. {
  633. $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
  634. }
  635. }
  636. else
  637. {
  638. // The value has to be found or verified
  639. list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
  640. if (count($aMatches) == 1)
  641. {
  642. $oRemoteObj = reset($aMatches); // first item
  643. $valuecondition = $oRemoteObj->GetKey();
  644. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
  645. }
  646. elseif (count($aMatches) == 0)
  647. {
  648. $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
  649. }
  650. else
  651. {
  652. $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
  653. }
  654. }
  655. }
  656. else
  657. {
  658. // The value is given in the data row
  659. $iCol = $this->m_aAttList[$sAttCode];
  660. $valuecondition = $aRowData[$iCol];
  661. }
  662. if (is_null($valuecondition))
  663. {
  664. $bSkipQuery = true;
  665. }
  666. else
  667. {
  668. $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
  669. }
  670. }
  671. if ($bSkipQuery)
  672. {
  673. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile");
  674. }
  675. else
  676. {
  677. $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
  678. switch($oReconciliationSet->Count())
  679. {
  680. case 0:
  681. $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
  682. // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
  683. $aVisited[] = $oTargetObj->GetKey();
  684. break;
  685. case 1:
  686. $oTargetObj = $oReconciliationSet->Fetch();
  687. $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
  688. // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
  689. if (!is_null($this->m_sSynchroScope))
  690. {
  691. $aVisited[] = $oTargetObj->GetKey();
  692. }
  693. break;
  694. default:
  695. // Found several matches, ambiguous
  696. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation");
  697. $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
  698. $aResult[$iRow]["finalclass"]= 'n/a';
  699. }
  700. }
  701. // Whatever happened, do report the reconciliation values
  702. foreach($this->m_aAttList as $iCol)
  703. {
  704. if (!array_key_exists($iCol, $aResult[$iRow]))
  705. {
  706. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  707. }
  708. }
  709. foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts)
  710. {
  711. if (!array_key_exists($sAttCode, $aResult[$iRow]))
  712. {
  713. $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a');
  714. }
  715. foreach ($aForeignAtts as $sForeignAttCode => $iCol)
  716. {
  717. if (!array_key_exists($iCol, $aResult[$iRow]))
  718. {
  719. // The foreign attribute is one of our reconciliation key
  720. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  721. }
  722. }
  723. }
  724. }
  725. if (!is_null($this->m_sSynchroScope))
  726. {
  727. // Compute the delta between the scope and visited objects
  728. $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
  729. $oScopeSet = new DBObjectSet($oScopeSearch);
  730. while ($oObj = $oScopeSet->Fetch())
  731. {
  732. $iObj = $oObj->GetKey();
  733. if (!in_array($iObj, $aVisited))
  734. {
  735. $iRow++;
  736. $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
  737. }
  738. }
  739. }
  740. return $aResult;
  741. }
  742. /**
  743. * Display the history of bulk imports
  744. */
  745. static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false)
  746. {
  747. $sAjaxDivId = "CSVImportHistory";
  748. if (!$bFromAjax)
  749. {
  750. $oPage->add('<div id="'.$sAjaxDivId.'">');
  751. }
  752. $oPage->p(Dict::S('UI:History:BulkImports+'));
  753. $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'");
  754. $iQueryLimit = $bShowAll ? 0 : MetaModel::GetConfig()->GetMaxDisplayLimit() + 1;
  755. $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit);
  756. $oAppContext = new ApplicationContext();
  757. $bLimitExceeded = false;
  758. if ($oBulkChanges->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit())
  759. {
  760. $bLimitExceeded = true;
  761. if (!$bShowAll)
  762. {
  763. $iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit();
  764. $oBulkChanges->SetLimit($iMaxObjects);
  765. }
  766. }
  767. $oBulkChanges->Seek(0);
  768. $aDetails = array();
  769. while ($oChange = $oBulkChanges->Fetch())
  770. {
  771. $sDate = '<a href="?step=10&changeid='.$oChange->GetKey().'&'.$oAppContext->GetForLink().'">'.$oChange->Get('date').'</a>';
  772. $sUser = $oChange->GetUserName();
  773. if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches))
  774. {
  775. $sUser = $aMatches[1];
  776. }
  777. else
  778. {
  779. $sUser = $oChange->Get('userinfo');
  780. }
  781. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id");
  782. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  783. $iCreated = $oOpSet->Count();
  784. // Get the class from the first item found (assumption: a CSV load is done for a single class)
  785. if ($oCreateOp = $oOpSet->Fetch())
  786. {
  787. $sClass = $oCreateOp->Get('objclass');
  788. }
  789. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id");
  790. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  791. $aModified = array();
  792. $aAttList = array();
  793. while ($oModified = $oOpSet->Fetch())
  794. {
  795. // Get the class (if not done earlier on object creation)
  796. $sClass = $oModified->Get('objclass');
  797. $iKey = $oModified->Get('objkey');
  798. $sAttCode = $oModified->Get('attcode');
  799. $aAttList[$sClass][$sAttCode] = true;
  800. $aModified["$sClass::$iKey"] = true;
  801. }
  802. $iModified = count($aModified);
  803. // Assumption: there is only one class of objects being loaded
  804. // Then the last class found gives us the class for every object
  805. if ( ($iModified > 0) || ($iCreated > 0))
  806. {
  807. $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
  808. }
  809. }
  810. $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
  811. 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')),
  812. 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')),
  813. 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')),
  814. 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')),
  815. );
  816. if ($bLimitExceeded)
  817. {
  818. if ($bShowAll)
  819. {
  820. // Collapsible list
  821. $oPage->add('<p>'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(false);">'.Dict::S('UI:CollapseList').'</a></p>');
  822. }
  823. else
  824. {
  825. // Truncated list
  826. $iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit();
  827. $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
  828. $sLinkLabel = Dict::S('UI:DisplayAll');
  829. $oPage->add('<p>'.$sCollapsedLabel.'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(true);">'.$sLinkLabel.'</p>');
  830. $oPage->add_ready_script(
  831. <<<EOF
  832. $('#$sAjaxDivId table.listResults').addClass('truncated');
  833. $('#$sAjaxDivId table.listResults tr:last td').addClass('truncated');
  834. EOF
  835. );
  836. $sAppContext = $oAppContext->GetForLink();
  837. $oPage->add_script(
  838. <<<EOF
  839. function OnTruncatedHistoryToggle(bShowAll)
  840. {
  841. $.get('../pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
  842. {
  843. $('#$sAjaxDivId').html(data);
  844. var table = $('#$sAjaxDivId .listResults');
  845. table.tableHover(); // hover tables
  846. table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
  847. }
  848. );
  849. }
  850. EOF
  851. );
  852. }
  853. }
  854. else
  855. {
  856. // Normal display - full list without any decoration
  857. }
  858. $oPage->table($aConfig, $aDetails);
  859. if (!$bFromAjax)
  860. {
  861. $oPage->add('</div>');
  862. }
  863. }
  864. /**
  865. * Display the details of an import
  866. */
  867. static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange)
  868. {
  869. if ($iChange == 0)
  870. {
  871. throw new Exception("Missing parameter changeid");
  872. }
  873. $oChange = MetaModel::GetObject('CMDBChange', $iChange, false);
  874. if (is_null($oChange))
  875. {
  876. throw new Exception("Unknown change: $iChange");
  877. }
  878. $oPage->add("<div><p><h1>".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."</h1></p></div>\n");
  879. // Assumption : change made one single class of objects
  880. $aObjects = array();
  881. $aAttributes = array(); // array of attcode => occurences
  882. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id");
  883. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange));
  884. while ($oOperation = $oOpSet->Fetch())
  885. {
  886. $sClass = $oOperation->Get('objclass');
  887. $iKey = $oOperation->Get('objkey');
  888. $iObjId = "$sClass::$iKey";
  889. if (!isset($aObjects[$iObjId]))
  890. {
  891. $aObjects[$iObjId] = array();
  892. $aObjects[$iObjId]['__class__'] = $sClass;
  893. $aObjects[$iObjId]['__id__'] = $iKey;
  894. }
  895. if (get_class($oOperation) == 'CMDBChangeOpCreate')
  896. {
  897. $aObjects[$iObjId]['__created__'] = true;
  898. }
  899. elseif ($oOperation instanceof CMDBChangeOpSetAttribute)
  900. {
  901. $sAttCode = $oOperation->Get('attcode');
  902. if (get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar')
  903. {
  904. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  905. if ($oAttDef->IsExternalKey())
  906. {
  907. $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
  908. $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue'));
  909. $sOldValue = $oOldTarget->GetHyperlink();
  910. $sNewValue = $oNewTarget->GetHyperlink();
  911. }
  912. else
  913. {
  914. $sOldValue = $oOperation->GetAsHTML('oldvalue');
  915. $sNewValue = $oOperation->GetAsHTML('newvalue');
  916. }
  917. $aObjects[$iObjId][$sAttCode] = $sOldValue.' -&gt; '.$sNewValue;
  918. }
  919. else
  920. {
  921. $aObjects[$iObjId][$sAttCode] = 'n/a';
  922. }
  923. if (isset($aAttributes[$sAttCode]))
  924. {
  925. $aAttributes[$sAttCode]++;
  926. }
  927. else
  928. {
  929. $aAttributes[$sAttCode] = 1;
  930. }
  931. }
  932. }
  933. $aDetails = array();
  934. foreach($aObjects as $iUId => $aObjData)
  935. {
  936. $aRow = array();
  937. $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false);
  938. if (is_null($oObject))
  939. {
  940. $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)';
  941. }
  942. else
  943. {
  944. $aRow['object'] = $oObject->GetHyperlink();
  945. }
  946. if (isset($aObjData['__created__']))
  947. {
  948. $aRow['operation'] = Dict::S('Change:ObjectCreated');
  949. }
  950. else
  951. {
  952. $aRow['operation'] = Dict::S('Change:ObjectModified');
  953. }
  954. foreach ($aAttributes as $sAttCode => $iOccurences)
  955. {
  956. if (isset($aObjData[$sAttCode]))
  957. {
  958. $aRow[$sAttCode] = $aObjData[$sAttCode];
  959. }
  960. elseif (!is_null($oObject))
  961. {
  962. // This is the current vaslue: $oObject->GetAsHtml($sAttCode)
  963. // whereas we are displaying the value that was set at the time
  964. // the object was created
  965. // This requires addtional coding...let's do that later
  966. $aRow[$sAttCode] = '';
  967. }
  968. else
  969. {
  970. $aRow[$sAttCode] = '';
  971. }
  972. }
  973. $aDetails[] = $aRow;
  974. }
  975. $aConfig = array();
  976. $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass));
  977. $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+'));
  978. foreach ($aAttributes as $sAttCode => $iOccurences)
  979. {
  980. $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
  981. }
  982. $oPage->table($aConfig, $aDetails);
  983. }
  984. /**
  985. * Get the user friendly name for an 'extended' attribute code i.e 'name', becomes 'Name' and 'org_id->name' becomes 'Organization->Name'
  986. * @param string $sClassName The name of the class
  987. * @param string $sAttCodeEx Either an attribute code or ext_key_name->att_code
  988. * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName
  989. */
  990. public static function GetFriendlyAttCodeName($sClassName, $sAttCodeEx)
  991. {
  992. $sFriendlyName = '';
  993. if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0)
  994. {
  995. $sAttribute = $aMatches[1];
  996. $sField = $aMatches[2];
  997. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttribute);
  998. if ($oAttDef->IsExternalKey())
  999. {
  1000. $sTargetClass = $oAttDef->GetTargetClass();
  1001. $oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField);
  1002. $sFriendlyName = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel();
  1003. }
  1004. else
  1005. {
  1006. // hum, hum... should never happen, we'd better raise an exception
  1007. throw(new Exception(Dict::Format('UI:CSVImport:ErrorExtendedAttCode', $sAttCodeEx, $sAttribute, $sClassName)));
  1008. }
  1009. }
  1010. else
  1011. {
  1012. if ($sAttCodeEx == 'id')
  1013. {
  1014. $sFriendlyName = Dict::S('UI:CSVImport:idField');
  1015. }
  1016. else
  1017. {
  1018. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCodeEx);
  1019. $sFriendlyName = $oAttDef->GetLabel();
  1020. }
  1021. }
  1022. return $sFriendlyName;
  1023. }
  1024. }
  1025. ?>