bulkchange.class.inc.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  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. try
  551. {
  552. $oTargetObj->DBUpdateTracked($oChange);
  553. }
  554. catch(CoreException $e)
  555. {
  556. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage());
  557. }
  558. }
  559. }
  560. else
  561. {
  562. $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange();
  563. }
  564. }
  565. protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null)
  566. {
  567. $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors);
  568. // Reporting
  569. //
  570. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  571. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  572. if (count($aErrors) > 0)
  573. {
  574. $sErrors = implode(', ', $aErrors);
  575. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  576. return;
  577. }
  578. $aChangedFields = $oTargetObj->ListChanges();
  579. if (count($aChangedFields) > 0)
  580. {
  581. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields));
  582. // Optionaly record the results
  583. //
  584. if ($oChange)
  585. {
  586. try
  587. {
  588. $oTargetObj->DBUpdateTracked($oChange);
  589. }
  590. catch(CoreException $e)
  591. {
  592. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage());
  593. }
  594. }
  595. }
  596. else
  597. {
  598. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
  599. }
  600. }
  601. public function Process(CMDBChange $oChange = null)
  602. {
  603. // Note: $oChange can be null, in which case the aim is to check what would be done
  604. // Debug...
  605. //
  606. if (false)
  607. {
  608. echo "<pre>\n";
  609. echo "Attributes:\n";
  610. print_r($this->m_aAttList);
  611. echo "ExtKeys:\n";
  612. print_r($this->m_aExtKeys);
  613. echo "Reconciliation:\n";
  614. print_r($this->m_aReconcilKeys);
  615. echo "Synchro scope:\n";
  616. print_r($this->m_sSynchroScope);
  617. echo "Synchro changes:\n";
  618. print_r($this->m_aOnDisappear);
  619. //echo "Data:\n";
  620. //print_r($this->m_aData);
  621. echo "</pre>\n";
  622. exit;
  623. }
  624. // Compute the results
  625. //
  626. if (!is_null($this->m_sSynchroScope))
  627. {
  628. $aVisited = array();
  629. }
  630. $aResult = array();
  631. foreach($this->m_aData as $iRow => $aRowData)
  632. {
  633. $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass);
  634. $bSkipQuery = false;
  635. foreach($this->m_aReconcilKeys as $sAttCode)
  636. {
  637. $valuecondition = null;
  638. if (array_key_exists($sAttCode, $this->m_aExtKeys))
  639. {
  640. if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
  641. {
  642. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  643. if ($oExtKey->IsNullAllowed())
  644. {
  645. $valuecondition = $oExtKey->GetNullValue();
  646. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
  647. }
  648. else
  649. {
  650. $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
  651. }
  652. }
  653. else
  654. {
  655. // The value has to be found or verified
  656. list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
  657. if (count($aMatches) == 1)
  658. {
  659. $oRemoteObj = reset($aMatches); // first item
  660. $valuecondition = $oRemoteObj->GetKey();
  661. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
  662. }
  663. elseif (count($aMatches) == 0)
  664. {
  665. $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
  666. }
  667. else
  668. {
  669. $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
  670. }
  671. }
  672. }
  673. else
  674. {
  675. // The value is given in the data row
  676. $iCol = $this->m_aAttList[$sAttCode];
  677. $valuecondition = $aRowData[$iCol];
  678. }
  679. if (is_null($valuecondition))
  680. {
  681. $bSkipQuery = true;
  682. }
  683. else
  684. {
  685. $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
  686. }
  687. }
  688. if ($bSkipQuery)
  689. {
  690. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile");
  691. }
  692. else
  693. {
  694. $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
  695. switch($oReconciliationSet->Count())
  696. {
  697. case 0:
  698. $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
  699. // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
  700. $aVisited[] = $oTargetObj->GetKey();
  701. break;
  702. case 1:
  703. $oTargetObj = $oReconciliationSet->Fetch();
  704. $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
  705. // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
  706. if (!is_null($this->m_sSynchroScope))
  707. {
  708. $aVisited[] = $oTargetObj->GetKey();
  709. }
  710. break;
  711. default:
  712. // Found several matches, ambiguous
  713. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation");
  714. $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
  715. $aResult[$iRow]["finalclass"]= 'n/a';
  716. }
  717. }
  718. // Whatever happened, do report the reconciliation values
  719. foreach($this->m_aAttList as $iCol)
  720. {
  721. if (!array_key_exists($iCol, $aResult[$iRow]))
  722. {
  723. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  724. }
  725. }
  726. foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts)
  727. {
  728. if (!array_key_exists($sAttCode, $aResult[$iRow]))
  729. {
  730. $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a');
  731. }
  732. foreach ($aForeignAtts as $sForeignAttCode => $iCol)
  733. {
  734. if (!array_key_exists($iCol, $aResult[$iRow]))
  735. {
  736. // The foreign attribute is one of our reconciliation key
  737. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  738. }
  739. }
  740. }
  741. }
  742. if (!is_null($this->m_sSynchroScope))
  743. {
  744. // Compute the delta between the scope and visited objects
  745. $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
  746. $oScopeSet = new DBObjectSet($oScopeSearch);
  747. while ($oObj = $oScopeSet->Fetch())
  748. {
  749. $iObj = $oObj->GetKey();
  750. if (!in_array($iObj, $aVisited))
  751. {
  752. $iRow++;
  753. $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
  754. }
  755. }
  756. }
  757. return $aResult;
  758. }
  759. /**
  760. * Display the history of bulk imports
  761. */
  762. static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false)
  763. {
  764. $sAjaxDivId = "CSVImportHistory";
  765. if (!$bFromAjax)
  766. {
  767. $oPage->add('<div id="'.$sAjaxDivId.'">');
  768. }
  769. $oPage->p(Dict::S('UI:History:BulkImports+'));
  770. $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'");
  771. $iQueryLimit = $bShowAll ? 0 : MetaModel::GetConfig()->GetMaxDisplayLimit() + 1;
  772. $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit);
  773. $oAppContext = new ApplicationContext();
  774. $bLimitExceeded = false;
  775. if ($oBulkChanges->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit())
  776. {
  777. $bLimitExceeded = true;
  778. if (!$bShowAll)
  779. {
  780. $iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit();
  781. $oBulkChanges->SetLimit($iMaxObjects);
  782. }
  783. }
  784. $oBulkChanges->Seek(0);
  785. $aDetails = array();
  786. while ($oChange = $oBulkChanges->Fetch())
  787. {
  788. $sDate = '<a href="?step=10&changeid='.$oChange->GetKey().'&'.$oAppContext->GetForLink().'">'.$oChange->Get('date').'</a>';
  789. $sUser = $oChange->GetUserName();
  790. if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches))
  791. {
  792. $sUser = $aMatches[1];
  793. }
  794. else
  795. {
  796. $sUser = $oChange->Get('userinfo');
  797. }
  798. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id");
  799. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  800. $iCreated = $oOpSet->Count();
  801. // Get the class from the first item found (assumption: a CSV load is done for a single class)
  802. if ($oCreateOp = $oOpSet->Fetch())
  803. {
  804. $sClass = $oCreateOp->Get('objclass');
  805. }
  806. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id");
  807. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  808. $aModified = array();
  809. $aAttList = array();
  810. while ($oModified = $oOpSet->Fetch())
  811. {
  812. // Get the class (if not done earlier on object creation)
  813. $sClass = $oModified->Get('objclass');
  814. $iKey = $oModified->Get('objkey');
  815. $sAttCode = $oModified->Get('attcode');
  816. $aAttList[$sClass][$sAttCode] = true;
  817. $aModified["$sClass::$iKey"] = true;
  818. }
  819. $iModified = count($aModified);
  820. // Assumption: there is only one class of objects being loaded
  821. // Then the last class found gives us the class for every object
  822. if ( ($iModified > 0) || ($iCreated > 0))
  823. {
  824. $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
  825. }
  826. }
  827. $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
  828. 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')),
  829. 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')),
  830. 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')),
  831. 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')),
  832. );
  833. if ($bLimitExceeded)
  834. {
  835. if ($bShowAll)
  836. {
  837. // Collapsible list
  838. $oPage->add('<p>'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(false);">'.Dict::S('UI:CollapseList').'</a></p>');
  839. }
  840. else
  841. {
  842. // Truncated list
  843. $iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit();
  844. $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
  845. $sLinkLabel = Dict::S('UI:DisplayAll');
  846. $oPage->add('<p>'.$sCollapsedLabel.'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(true);">'.$sLinkLabel.'</p>');
  847. $oPage->add_ready_script(
  848. <<<EOF
  849. $('#$sAjaxDivId table.listResults').addClass('truncated');
  850. $('#$sAjaxDivId table.listResults tr:last td').addClass('truncated');
  851. EOF
  852. );
  853. $sAppContext = $oAppContext->GetForLink();
  854. $oPage->add_script(
  855. <<<EOF
  856. function OnTruncatedHistoryToggle(bShowAll)
  857. {
  858. $.get('../pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
  859. {
  860. $('#$sAjaxDivId').html(data);
  861. var table = $('#$sAjaxDivId .listResults');
  862. table.tableHover(); // hover tables
  863. table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
  864. }
  865. );
  866. }
  867. EOF
  868. );
  869. }
  870. }
  871. else
  872. {
  873. // Normal display - full list without any decoration
  874. }
  875. $oPage->table($aConfig, $aDetails);
  876. if (!$bFromAjax)
  877. {
  878. $oPage->add('</div>');
  879. }
  880. }
  881. /**
  882. * Display the details of an import
  883. */
  884. static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange)
  885. {
  886. if ($iChange == 0)
  887. {
  888. throw new Exception("Missing parameter changeid");
  889. }
  890. $oChange = MetaModel::GetObject('CMDBChange', $iChange, false);
  891. if (is_null($oChange))
  892. {
  893. throw new Exception("Unknown change: $iChange");
  894. }
  895. $oPage->add("<div><p><h1>".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."</h1></p></div>\n");
  896. // Assumption : change made one single class of objects
  897. $aObjects = array();
  898. $aAttributes = array(); // array of attcode => occurences
  899. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id");
  900. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange));
  901. while ($oOperation = $oOpSet->Fetch())
  902. {
  903. $sClass = $oOperation->Get('objclass');
  904. $iKey = $oOperation->Get('objkey');
  905. $iObjId = "$sClass::$iKey";
  906. if (!isset($aObjects[$iObjId]))
  907. {
  908. $aObjects[$iObjId] = array();
  909. $aObjects[$iObjId]['__class__'] = $sClass;
  910. $aObjects[$iObjId]['__id__'] = $iKey;
  911. }
  912. if (get_class($oOperation) == 'CMDBChangeOpCreate')
  913. {
  914. $aObjects[$iObjId]['__created__'] = true;
  915. }
  916. elseif ($oOperation instanceof CMDBChangeOpSetAttribute)
  917. {
  918. $sAttCode = $oOperation->Get('attcode');
  919. if (get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar')
  920. {
  921. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  922. if ($oAttDef->IsExternalKey())
  923. {
  924. $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
  925. $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue'));
  926. $sOldValue = $oOldTarget->GetHyperlink();
  927. $sNewValue = $oNewTarget->GetHyperlink();
  928. }
  929. else
  930. {
  931. $sOldValue = $oOperation->GetAsHTML('oldvalue');
  932. $sNewValue = $oOperation->GetAsHTML('newvalue');
  933. }
  934. $aObjects[$iObjId][$sAttCode] = $sOldValue.' -&gt; '.$sNewValue;
  935. }
  936. else
  937. {
  938. $aObjects[$iObjId][$sAttCode] = 'n/a';
  939. }
  940. if (isset($aAttributes[$sAttCode]))
  941. {
  942. $aAttributes[$sAttCode]++;
  943. }
  944. else
  945. {
  946. $aAttributes[$sAttCode] = 1;
  947. }
  948. }
  949. }
  950. $aDetails = array();
  951. foreach($aObjects as $iUId => $aObjData)
  952. {
  953. $aRow = array();
  954. $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false);
  955. if (is_null($oObject))
  956. {
  957. $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)';
  958. }
  959. else
  960. {
  961. $aRow['object'] = $oObject->GetHyperlink();
  962. }
  963. if (isset($aObjData['__created__']))
  964. {
  965. $aRow['operation'] = Dict::S('Change:ObjectCreated');
  966. }
  967. else
  968. {
  969. $aRow['operation'] = Dict::S('Change:ObjectModified');
  970. }
  971. foreach ($aAttributes as $sAttCode => $iOccurences)
  972. {
  973. if (isset($aObjData[$sAttCode]))
  974. {
  975. $aRow[$sAttCode] = $aObjData[$sAttCode];
  976. }
  977. elseif (!is_null($oObject))
  978. {
  979. // This is the current vaslue: $oObject->GetAsHtml($sAttCode)
  980. // whereas we are displaying the value that was set at the time
  981. // the object was created
  982. // This requires addtional coding...let's do that later
  983. $aRow[$sAttCode] = '';
  984. }
  985. else
  986. {
  987. $aRow[$sAttCode] = '';
  988. }
  989. }
  990. $aDetails[] = $aRow;
  991. }
  992. $aConfig = array();
  993. $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass));
  994. $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+'));
  995. foreach ($aAttributes as $sAttCode => $iOccurences)
  996. {
  997. $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
  998. }
  999. $oPage->table($aConfig, $aDetails);
  1000. }
  1001. /**
  1002. * Get the user friendly name for an 'extended' attribute code i.e 'name', becomes 'Name' and 'org_id->name' becomes 'Organization->Name'
  1003. * @param string $sClassName The name of the class
  1004. * @param string $sAttCodeEx Either an attribute code or ext_key_name->att_code
  1005. * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName
  1006. */
  1007. public static function GetFriendlyAttCodeName($sClassName, $sAttCodeEx)
  1008. {
  1009. $sFriendlyName = '';
  1010. if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0)
  1011. {
  1012. $sAttribute = $aMatches[1];
  1013. $sField = $aMatches[2];
  1014. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttribute);
  1015. if ($oAttDef->IsExternalKey())
  1016. {
  1017. $sTargetClass = $oAttDef->GetTargetClass();
  1018. $oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField);
  1019. $sFriendlyName = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel();
  1020. }
  1021. else
  1022. {
  1023. // hum, hum... should never happen, we'd better raise an exception
  1024. throw(new Exception(Dict::Format('UI:CSVImport:ErrorExtendedAttCode', $sAttCodeEx, $sAttribute, $sClassName)));
  1025. }
  1026. }
  1027. else
  1028. {
  1029. if ($sAttCodeEx == 'id')
  1030. {
  1031. $sFriendlyName = Dict::S('UI:CSVImport:idField');
  1032. }
  1033. else
  1034. {
  1035. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCodeEx);
  1036. $sFriendlyName = $oAttDef->GetLabel();
  1037. }
  1038. }
  1039. return $sFriendlyName;
  1040. }
  1041. }
  1042. ?>