bulkchange.class.inc.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  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() && $oAttDef->IsIndirect())
  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. else
  383. {
  384. if ($this->m_bReportHtml)
  385. {
  386. $sCurValue = $oTargetObj->GetAsHTML($sAttCode);
  387. $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode);
  388. }
  389. else
  390. {
  391. $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
  392. $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
  393. }
  394. if (isset($aErrors[$sAttCode]))
  395. {
  396. $aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]);
  397. }
  398. elseif (array_key_exists($sAttCode, $aChangedFields))
  399. {
  400. if ($oTargetObj->IsNew())
  401. {
  402. $aResults[$iCol]= new CellStatus_Void($sCurValue);
  403. }
  404. else
  405. {
  406. $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue);
  407. }
  408. }
  409. else
  410. {
  411. // By default... nothing happens
  412. $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
  413. }
  414. }
  415. }
  416. // Checks
  417. //
  418. $res = $oTargetObj->CheckConsistency();
  419. if ($res !== true)
  420. {
  421. // $res contains the error description
  422. $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res";
  423. }
  424. return $aResults;
  425. }
  426. protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
  427. {
  428. $aResults = array();
  429. $aErrors = array();
  430. // External keys
  431. //
  432. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  433. {
  434. //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
  435. $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode));
  436. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  437. {
  438. $aResults[$iCol] = new CellStatus_Void('?');
  439. }
  440. }
  441. // Update attributes
  442. //
  443. foreach($this->m_aOnDisappear as $sAttCode => $value)
  444. {
  445. if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode))
  446. {
  447. throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode));
  448. }
  449. $oTargetObj->Set($sAttCode, $value);
  450. if (!array_key_exists($sAttCode, $this->m_aAttList))
  451. {
  452. // #@# will be out of the reporting... (counted anyway)
  453. }
  454. }
  455. // Reporting on fields
  456. //
  457. $aChangedFields = $oTargetObj->ListChanges();
  458. foreach ($this->m_aAttList as $sAttCode => $iCol)
  459. {
  460. if ($sAttCode == 'id')
  461. {
  462. $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey());
  463. }
  464. if (array_key_exists($sAttCode, $aChangedFields))
  465. {
  466. $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode));
  467. }
  468. else
  469. {
  470. // By default... nothing happens
  471. $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode));
  472. }
  473. }
  474. // Checks
  475. //
  476. $res = $oTargetObj->CheckConsistency();
  477. if ($res !== true)
  478. {
  479. // $res contains the error description
  480. $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res";
  481. }
  482. return $aResults;
  483. }
  484. protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null)
  485. {
  486. $oTargetObj = MetaModel::NewObject($this->m_sClass);
  487. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  488. if (count($aErrors) > 0)
  489. {
  490. $sErrors = implode(', ', $aErrors);
  491. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  492. return $oTargetObj;
  493. }
  494. // Check that any external key will have a value proposed
  495. $aMissingKeys = array();
  496. foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey)
  497. {
  498. if (!$oExtKey->IsNullAllowed())
  499. {
  500. if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList))
  501. {
  502. $aMissingKeys[] = $oExtKey->GetLabel();
  503. }
  504. }
  505. }
  506. if (count($aMissingKeys) > 0)
  507. {
  508. $sMissingKeys = implode(', ', $aMissingKeys);
  509. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Could not be created, due to missing external key(s): $sMissingKeys");
  510. return $oTargetObj;
  511. }
  512. // Optionaly record the results
  513. //
  514. if ($oChange)
  515. {
  516. $newID = $oTargetObj->DBInsertTrackedNoReload($oChange);
  517. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj($this->m_sClass, $newID);
  518. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  519. $aResult[$iRow]["id"] = new CellStatus_Void($newID);
  520. }
  521. else
  522. {
  523. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj();
  524. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  525. $aResult[$iRow]["id"] = new CellStatus_Void(0);
  526. }
  527. return $oTargetObj;
  528. }
  529. protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null)
  530. {
  531. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  532. // Reporting
  533. //
  534. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  535. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  536. if (count($aErrors) > 0)
  537. {
  538. $sErrors = implode(', ', $aErrors);
  539. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  540. return;
  541. }
  542. $aChangedFields = $oTargetObj->ListChanges();
  543. if (count($aChangedFields) > 0)
  544. {
  545. $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
  546. // Optionaly record the results
  547. //
  548. if ($oChange)
  549. {
  550. $oTargetObj->DBUpdateTracked($oChange);
  551. }
  552. }
  553. else
  554. {
  555. $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange();
  556. }
  557. }
  558. protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null)
  559. {
  560. $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors);
  561. // Reporting
  562. //
  563. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  564. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  565. if (count($aErrors) > 0)
  566. {
  567. $sErrors = implode(', ', $aErrors);
  568. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  569. return;
  570. }
  571. $aChangedFields = $oTargetObj->ListChanges();
  572. if (count($aChangedFields) > 0)
  573. {
  574. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields));
  575. // Optionaly record the results
  576. //
  577. if ($oChange)
  578. {
  579. $oTargetObj->DBUpdateTracked($oChange);
  580. }
  581. }
  582. else
  583. {
  584. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
  585. }
  586. }
  587. public function Process(CMDBChange $oChange = null)
  588. {
  589. // Note: $oChange can be null, in which case the aim is to check what would be done
  590. // Debug...
  591. //
  592. if (false)
  593. {
  594. echo "<pre>\n";
  595. echo "Attributes:\n";
  596. print_r($this->m_aAttList);
  597. echo "ExtKeys:\n";
  598. print_r($this->m_aExtKeys);
  599. echo "Reconciliation:\n";
  600. print_r($this->m_aReconcilKeys);
  601. echo "Synchro scope:\n";
  602. print_r($this->m_sSynchroScope);
  603. echo "Synchro changes:\n";
  604. print_r($this->m_aOnDisappear);
  605. //echo "Data:\n";
  606. //print_r($this->m_aData);
  607. echo "</pre>\n";
  608. exit;
  609. }
  610. // Compute the results
  611. //
  612. if (!is_null($this->m_sSynchroScope))
  613. {
  614. $aVisited = array();
  615. }
  616. $aResult = array();
  617. foreach($this->m_aData as $iRow => $aRowData)
  618. {
  619. $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass);
  620. $bSkipQuery = false;
  621. foreach($this->m_aReconcilKeys as $sAttCode)
  622. {
  623. $valuecondition = null;
  624. if (array_key_exists($sAttCode, $this->m_aExtKeys))
  625. {
  626. if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
  627. {
  628. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  629. if ($oExtKey->IsNullAllowed())
  630. {
  631. $valuecondition = $oExtKey->GetNullValue();
  632. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
  633. }
  634. else
  635. {
  636. $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
  637. }
  638. }
  639. else
  640. {
  641. // The value has to be found or verified
  642. list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
  643. if (count($aMatches) == 1)
  644. {
  645. $oRemoteObj = reset($aMatches); // first item
  646. $valuecondition = $oRemoteObj->GetKey();
  647. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
  648. }
  649. elseif (count($aMatches) == 0)
  650. {
  651. $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
  652. }
  653. else
  654. {
  655. $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
  656. }
  657. }
  658. }
  659. else
  660. {
  661. // The value is given in the data row
  662. $iCol = $this->m_aAttList[$sAttCode];
  663. $valuecondition = $aRowData[$iCol];
  664. }
  665. if (is_null($valuecondition))
  666. {
  667. $bSkipQuery = true;
  668. }
  669. else
  670. {
  671. $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
  672. }
  673. }
  674. if ($bSkipQuery)
  675. {
  676. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile");
  677. }
  678. else
  679. {
  680. $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
  681. switch($oReconciliationSet->Count())
  682. {
  683. case 0:
  684. $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
  685. // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
  686. $aVisited[] = $oTargetObj->GetKey();
  687. break;
  688. case 1:
  689. $oTargetObj = $oReconciliationSet->Fetch();
  690. $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
  691. // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
  692. if (!is_null($this->m_sSynchroScope))
  693. {
  694. $aVisited[] = $oTargetObj->GetKey();
  695. }
  696. break;
  697. default:
  698. // Found several matches, ambiguous
  699. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation");
  700. $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
  701. $aResult[$iRow]["finalclass"]= 'n/a';
  702. }
  703. }
  704. // Whatever happened, do report the reconciliation values
  705. foreach($this->m_aAttList as $iCol)
  706. {
  707. if (!array_key_exists($iCol, $aResult[$iRow]))
  708. {
  709. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  710. }
  711. }
  712. foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts)
  713. {
  714. if (!array_key_exists($sAttCode, $aResult[$iRow]))
  715. {
  716. $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a');
  717. }
  718. foreach ($aForeignAtts as $sForeignAttCode => $iCol)
  719. {
  720. if (!array_key_exists($iCol, $aResult[$iRow]))
  721. {
  722. // The foreign attribute is one of our reconciliation key
  723. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  724. }
  725. }
  726. }
  727. }
  728. if (!is_null($this->m_sSynchroScope))
  729. {
  730. // Compute the delta between the scope and visited objects
  731. $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
  732. $oScopeSet = new DBObjectSet($oScopeSearch);
  733. while ($oObj = $oScopeSet->Fetch())
  734. {
  735. $iObj = $oObj->GetKey();
  736. if (!in_array($iObj, $aVisited))
  737. {
  738. $iRow++;
  739. $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
  740. }
  741. }
  742. }
  743. return $aResult;
  744. }
  745. /**
  746. * Display the history of bulk imports
  747. */
  748. static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false)
  749. {
  750. $sAjaxDivId = "CSVImportHistory";
  751. if (!$bFromAjax)
  752. {
  753. $oPage->add('<div id="'.$sAjaxDivId.'">');
  754. }
  755. $oPage->p(Dict::S('UI:History:BulkImports+'));
  756. $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'");
  757. $iQueryLimit = $bShowAll ? 0 : MetaModel::GetConfig()->GetMaxDisplayLimit() + 1;
  758. $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit);
  759. $oAppContext = new ApplicationContext();
  760. $bLimitExceeded = false;
  761. if ($oBulkChanges->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit())
  762. {
  763. $bLimitExceeded = true;
  764. if (!$bShowAll)
  765. {
  766. $iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit();
  767. $oBulkChanges->SetLimit($iMaxObjects);
  768. }
  769. }
  770. $oBulkChanges->Seek(0);
  771. $aDetails = array();
  772. while ($oChange = $oBulkChanges->Fetch())
  773. {
  774. $sDate = '<a href="?step=10&changeid='.$oChange->GetKey().'&'.$oAppContext->GetForLink().'">'.$oChange->Get('date').'</a>';
  775. $sUser = $oChange->GetUserName();
  776. if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches))
  777. {
  778. $sUser = $aMatches[1];
  779. }
  780. else
  781. {
  782. $sUser = $oChange->Get('userinfo');
  783. }
  784. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id");
  785. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  786. $iCreated = $oOpSet->Count();
  787. // Get the class from the first item found (assumption: a CSV load is done for a single class)
  788. if ($oCreateOp = $oOpSet->Fetch())
  789. {
  790. $sClass = $oCreateOp->Get('objclass');
  791. }
  792. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id");
  793. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  794. $aModified = array();
  795. $aAttList = array();
  796. while ($oModified = $oOpSet->Fetch())
  797. {
  798. // Get the class (if not done earlier on object creation)
  799. $sClass = $oModified->Get('objclass');
  800. $iKey = $oModified->Get('objkey');
  801. $sAttCode = $oModified->Get('attcode');
  802. $aAttList[$sClass][$sAttCode] = true;
  803. $aModified["$sClass::$iKey"] = true;
  804. }
  805. $iModified = count($aModified);
  806. // Assumption: there is only one class of objects being loaded
  807. // Then the last class found gives us the class for every object
  808. if ( ($iModified > 0) || ($iCreated > 0))
  809. {
  810. $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
  811. }
  812. }
  813. $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
  814. 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')),
  815. 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')),
  816. 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')),
  817. 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')),
  818. );
  819. if ($bLimitExceeded)
  820. {
  821. if ($bShowAll)
  822. {
  823. // Collapsible list
  824. $oPage->add('<p>'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(false);">'.Dict::S('UI:CollapseList').'</a></p>');
  825. }
  826. else
  827. {
  828. // Truncated list
  829. $iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit();
  830. $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
  831. $sLinkLabel = Dict::S('UI:DisplayAll');
  832. $oPage->add('<p>'.$sCollapsedLabel.'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(true);">'.$sLinkLabel.'</p>');
  833. $oPage->add_ready_script(
  834. <<<EOF
  835. $('#$sAjaxDivId table.listResults').addClass('truncated');
  836. $('#$sAjaxDivId table.listResults tr:last td').addClass('truncated');
  837. EOF
  838. );
  839. $sAppContext = $oAppContext->GetForLink();
  840. $oPage->add_script(
  841. <<<EOF
  842. function OnTruncatedHistoryToggle(bShowAll)
  843. {
  844. $.get('../pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
  845. {
  846. $('#$sAjaxDivId').html(data);
  847. var table = $('#$sAjaxDivId .listResults');
  848. table.tableHover(); // hover tables
  849. table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
  850. }
  851. );
  852. }
  853. EOF
  854. );
  855. }
  856. }
  857. else
  858. {
  859. // Normal display - full list without any decoration
  860. }
  861. $oPage->table($aConfig, $aDetails);
  862. if (!$bFromAjax)
  863. {
  864. $oPage->add('</div>');
  865. }
  866. }
  867. /**
  868. * Display the details of an import
  869. */
  870. static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange)
  871. {
  872. if ($iChange == 0)
  873. {
  874. throw new Exception("Missing parameter changeid");
  875. }
  876. $oChange = MetaModel::GetObject('CMDBChange', $iChange, false);
  877. if (is_null($oChange))
  878. {
  879. throw new Exception("Unknown change: $iChange");
  880. }
  881. $oPage->add("<div><p><h1>".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."</h1></p></div>\n");
  882. // Assumption : change made one single class of objects
  883. $aObjects = array();
  884. $aAttributes = array(); // array of attcode => occurences
  885. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id");
  886. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange));
  887. while ($oOperation = $oOpSet->Fetch())
  888. {
  889. $sClass = $oOperation->Get('objclass');
  890. $iKey = $oOperation->Get('objkey');
  891. $iObjId = "$sClass::$iKey";
  892. if (!isset($aObjects[$iObjId]))
  893. {
  894. $aObjects[$iObjId] = array();
  895. $aObjects[$iObjId]['__class__'] = $sClass;
  896. $aObjects[$iObjId]['__id__'] = $iKey;
  897. }
  898. if (get_class($oOperation) == 'CMDBChangeOpCreate')
  899. {
  900. $aObjects[$iObjId]['__created__'] = true;
  901. }
  902. elseif ($oOperation instanceof CMDBChangeOpSetAttribute)
  903. {
  904. $sAttCode = $oOperation->Get('attcode');
  905. if (get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar')
  906. {
  907. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  908. if ($oAttDef->IsExternalKey())
  909. {
  910. $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
  911. $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue'));
  912. $sOldValue = $oOldTarget->GetHyperlink();
  913. $sNewValue = $oNewTarget->GetHyperlink();
  914. }
  915. else
  916. {
  917. $sOldValue = $oOperation->GetAsHTML('oldvalue');
  918. $sNewValue = $oOperation->GetAsHTML('newvalue');
  919. }
  920. $aObjects[$iObjId][$sAttCode] = $sOldValue.' -&gt; '.$sNewValue;
  921. }
  922. else
  923. {
  924. $aObjects[$iObjId][$sAttCode] = 'n/a';
  925. }
  926. if (isset($aAttributes[$sAttCode]))
  927. {
  928. $aAttributes[$sAttCode]++;
  929. }
  930. else
  931. {
  932. $aAttributes[$sAttCode] = 1;
  933. }
  934. }
  935. }
  936. $aDetails = array();
  937. foreach($aObjects as $iUId => $aObjData)
  938. {
  939. $aRow = array();
  940. $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false);
  941. if (is_null($oObject))
  942. {
  943. $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)';
  944. }
  945. else
  946. {
  947. $aRow['object'] = $oObject->GetHyperlink();
  948. }
  949. if (isset($aObjData['__created__']))
  950. {
  951. $aRow['operation'] = Dict::S('Change:ObjectCreated');
  952. }
  953. else
  954. {
  955. $aRow['operation'] = Dict::S('Change:ObjectModified');
  956. }
  957. foreach ($aAttributes as $sAttCode => $iOccurences)
  958. {
  959. if (isset($aObjData[$sAttCode]))
  960. {
  961. $aRow[$sAttCode] = $aObjData[$sAttCode];
  962. }
  963. elseif (!is_null($oObject))
  964. {
  965. // This is the current vaslue: $oObject->GetAsHtml($sAttCode)
  966. // whereas we are displaying the value that was set at the time
  967. // the object was created
  968. // This requires addtional coding...let's do that later
  969. $aRow[$sAttCode] = '';
  970. }
  971. else
  972. {
  973. $aRow[$sAttCode] = '';
  974. }
  975. }
  976. $aDetails[] = $aRow;
  977. }
  978. $aConfig = array();
  979. $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass));
  980. $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+'));
  981. foreach ($aAttributes as $sAttCode => $iOccurences)
  982. {
  983. $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
  984. }
  985. $oPage->table($aConfig, $aDetails);
  986. }
  987. /**
  988. * Get the user friendly name for an 'extended' attribute code i.e 'name', becomes 'Name' and 'org_id->name' becomes 'Organization->Name'
  989. * @param string $sClassName The name of the class
  990. * @param string $sAttCodeEx Either an attribute code or ext_key_name->att_code
  991. * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName
  992. */
  993. public static function GetFriendlyAttCodeName($sClassName, $sAttCodeEx)
  994. {
  995. $sFriendlyName = '';
  996. if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0)
  997. {
  998. $sAttribute = $aMatches[1];
  999. $sField = $aMatches[2];
  1000. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttribute);
  1001. if ($oAttDef->IsExternalKey())
  1002. {
  1003. $sTargetClass = $oAttDef->GetTargetClass();
  1004. $oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField);
  1005. $sFriendlyName = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel();
  1006. }
  1007. else
  1008. {
  1009. // hum, hum... should never happen, we'd better raise an exception
  1010. throw(new Exception(Dict::Format('UI:CSVImport:ErrorExtendedAttCode', $sAttCodeEx, $sAttribute, $sClassName)));
  1011. }
  1012. }
  1013. else
  1014. {
  1015. if ($sAttCodeEx == 'id')
  1016. {
  1017. $sFriendlyName = Dict::S('UI:CSVImport:idField');
  1018. }
  1019. else
  1020. {
  1021. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCodeEx);
  1022. $sFriendlyName = $oAttDef->GetLabel();
  1023. }
  1024. }
  1025. return $sFriendlyName;
  1026. }
  1027. }
  1028. ?>