bulkchange.class.inc.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  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. protected $m_sDateFormat; // Date format specification, see utils::StringToTime()
  215. public function __construct($sClass, $aData, $aAttList, $aExtKeys, $aReconcilKeys, $sSynchroScope = null, $aOnDisappear = null, $sDateFormat = null)
  216. {
  217. $this->m_sClass = $sClass;
  218. $this->m_aData = $aData;
  219. $this->m_aAttList = $aAttList;
  220. $this->m_aReconcilKeys = $aReconcilKeys;
  221. $this->m_aExtKeys = $aExtKeys;
  222. $this->m_sSynchroScope = $sSynchroScope;
  223. $this->m_aOnDisappear = $aOnDisappear;
  224. $this->m_sDateFormat = $sDateFormat;
  225. }
  226. protected $m_bReportHtml = false;
  227. protected $m_sReportCsvSep = ',';
  228. protected $m_sReportCsvDelimiter = '"';
  229. public function SetReportHtml()
  230. {
  231. $this->m_bReportHtml = true;
  232. }
  233. public function SetReportCsv($sSeparator = ',', $sDelimiter = '"')
  234. {
  235. $this->m_bReportHtml = false;
  236. $this->m_sReportCsvSep = $sSeparator;
  237. $this->m_sReportCsvDelimiter = $sDelimiter;
  238. }
  239. protected function ResolveExternalKey($aRowData, $sAttCode, &$aResults)
  240. {
  241. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  242. $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
  243. foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
  244. {
  245. // The foreign attribute is one of our reconciliation key
  246. $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
  247. $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
  248. }
  249. $oExtObjects = new CMDBObjectSet($oReconFilter);
  250. $aKeys = $oExtObjects->ToArray();
  251. return array($oReconFilter->ToOql(), $aKeys);
  252. }
  253. // Returns true if the CSV data specifies that the external key must be left undefined
  254. protected function IsNullExternalKeySpec($aRowData, $sAttCode)
  255. {
  256. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  257. foreach ($this->m_aExtKeys[$sAttCode] as $sForeignAttCode => $iCol)
  258. {
  259. // The foreign attribute is one of our reconciliation key
  260. if (strlen($aRowData[$iCol]) > 0)
  261. {
  262. return false;
  263. }
  264. }
  265. return true;
  266. }
  267. protected function PrepareObject(&$oTargetObj, $aRowData, &$aErrors)
  268. {
  269. $aResults = array();
  270. $aErrors = array();
  271. // External keys reconciliation
  272. //
  273. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  274. {
  275. // Skip external keys used for the reconciliation process
  276. // if (!array_key_exists($sAttCode, $this->m_aAttList)) continue;
  277. $oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
  278. if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
  279. {
  280. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  281. {
  282. $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
  283. }
  284. if ($oExtKey->IsNullAllowed())
  285. {
  286. $oTargetObj->Set($sAttCode, $oExtKey->GetNullValue());
  287. $aResults[$sAttCode]= new CellStatus_Void($oExtKey->GetNullValue());
  288. }
  289. else
  290. {
  291. $aErrors[$sAttCode] = "Null not allowed";
  292. $aResults[$sAttCode]= new CellStatus_Issue(null, $oTargetObj->Get($sAttCode), 'Null not allowed');
  293. }
  294. }
  295. else
  296. {
  297. $oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
  298. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  299. {
  300. // The foreign attribute is one of our reconciliation key
  301. $oReconFilter->AddCondition($sForeignAttCode, $aRowData[$iCol], '=');
  302. $aResults[$iCol] = new CellStatus_Void($aRowData[$iCol]);
  303. }
  304. $oExtObjects = new CMDBObjectSet($oReconFilter);
  305. switch($oExtObjects->Count())
  306. {
  307. case 0:
  308. $aErrors[$sAttCode] = "Object not found";
  309. $aResults[$sAttCode]= new CellStatus_SearchIssue();
  310. break;
  311. case 1:
  312. // Do change the external key attribute
  313. $oForeignObj = $oExtObjects->Fetch();
  314. $oTargetObj->Set($sAttCode, $oForeignObj->GetKey());
  315. break;
  316. default:
  317. $aErrors[$sAttCode] = "Found ".$oExtObjects->Count()." matches";
  318. $aResults[$sAttCode]= new CellStatus_Ambiguous($oTargetObj->Get($sAttCode), $oExtObjects->Count(), $oReconFilter->ToOql());
  319. }
  320. }
  321. // Report
  322. if (!array_key_exists($sAttCode, $aResults))
  323. {
  324. $iForeignObj = $oTargetObj->Get($sAttCode);
  325. if (array_key_exists($sAttCode, $oTargetObj->ListChanges()))
  326. {
  327. if ($oTargetObj->IsNew())
  328. {
  329. $aResults[$sAttCode]= new CellStatus_Void($iForeignObj);
  330. }
  331. else
  332. {
  333. $aResults[$sAttCode]= new CellStatus_Modify($iForeignObj, $oTargetObj->GetOriginal($sAttCode));
  334. }
  335. }
  336. else
  337. {
  338. $aResults[$sAttCode]= new CellStatus_Void($iForeignObj);
  339. }
  340. }
  341. }
  342. // Set the object attributes
  343. //
  344. foreach ($this->m_aAttList as $sAttCode => $iCol)
  345. {
  346. // skip the private key, if any
  347. if ($sAttCode == 'id') continue;
  348. $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  349. if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect())
  350. {
  351. try
  352. {
  353. $oSet = $oAttDef->MakeValueFromString($aRowData[$iCol]);
  354. $oTargetObj->Set($sAttCode, $oSet);
  355. }
  356. catch(CoreException $e)
  357. {
  358. $aErrors[$sAttCode] = "Failed to process input: ".$e->getMessage();
  359. }
  360. }
  361. else
  362. {
  363. $res = $oTargetObj->CheckValue($sAttCode, $aRowData[$iCol]);
  364. if ($res === true)
  365. {
  366. $oTargetObj->Set($sAttCode, $aRowData[$iCol]);
  367. }
  368. else
  369. {
  370. // $res is a string with the error description
  371. $aErrors[$sAttCode] = "Unexpected value for attribute '$sAttCode': $res";
  372. }
  373. }
  374. }
  375. // Reporting on fields
  376. //
  377. $aChangedFields = $oTargetObj->ListChanges();
  378. foreach ($this->m_aAttList as $sAttCode => $iCol)
  379. {
  380. if ($sAttCode == 'id')
  381. {
  382. $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
  383. }
  384. else
  385. {
  386. if ($this->m_bReportHtml)
  387. {
  388. $sCurValue = $oTargetObj->GetAsHTML($sAttCode);
  389. $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode);
  390. }
  391. else
  392. {
  393. $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
  394. $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter);
  395. }
  396. if (isset($aErrors[$sAttCode]))
  397. {
  398. $aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]);
  399. }
  400. elseif (array_key_exists($sAttCode, $aChangedFields))
  401. {
  402. if ($oTargetObj->IsNew())
  403. {
  404. $aResults[$iCol]= new CellStatus_Void($sCurValue);
  405. }
  406. else
  407. {
  408. $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue);
  409. }
  410. }
  411. else
  412. {
  413. // By default... nothing happens
  414. $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]);
  415. }
  416. }
  417. }
  418. // Checks
  419. //
  420. $res = $oTargetObj->CheckConsistency();
  421. if ($res !== true)
  422. {
  423. // $res contains the error description
  424. $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res";
  425. }
  426. return $aResults;
  427. }
  428. protected function PrepareMissingObject(&$oTargetObj, &$aErrors)
  429. {
  430. $aResults = array();
  431. $aErrors = array();
  432. // External keys
  433. //
  434. foreach($this->m_aExtKeys as $sAttCode => $aKeyConfig)
  435. {
  436. //$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
  437. $aResults[$sAttCode]= new CellStatus_Void($oTargetObj->Get($sAttCode));
  438. foreach ($aKeyConfig as $sForeignAttCode => $iCol)
  439. {
  440. $aResults[$iCol] = new CellStatus_Void('?');
  441. }
  442. }
  443. // Update attributes
  444. //
  445. foreach($this->m_aOnDisappear as $sAttCode => $value)
  446. {
  447. if (!MetaModel::IsValidAttCode(get_class($oTargetObj), $sAttCode))
  448. {
  449. throw new BulkChangeException('Invalid attribute code', array('class' => get_class($oTargetObj), 'attcode' => $sAttCode));
  450. }
  451. $oTargetObj->Set($sAttCode, $value);
  452. if (!array_key_exists($sAttCode, $this->m_aAttList))
  453. {
  454. // #@# will be out of the reporting... (counted anyway)
  455. }
  456. }
  457. // Reporting on fields
  458. //
  459. $aChangedFields = $oTargetObj->ListChanges();
  460. foreach ($this->m_aAttList as $sAttCode => $iCol)
  461. {
  462. if ($sAttCode == 'id')
  463. {
  464. $aResults[$iCol]= new CellStatus_Void($oTargetObj->GetKey());
  465. }
  466. if (array_key_exists($sAttCode, $aChangedFields))
  467. {
  468. $aResults[$iCol]= new CellStatus_Modify($oTargetObj->Get($sAttCode), $oTargetObj->GetOriginal($sAttCode));
  469. }
  470. else
  471. {
  472. // By default... nothing happens
  473. $aResults[$iCol]= new CellStatus_Void($oTargetObj->Get($sAttCode));
  474. }
  475. }
  476. // Checks
  477. //
  478. $res = $oTargetObj->CheckConsistency();
  479. if ($res !== true)
  480. {
  481. // $res contains the error description
  482. $aErrors["GLOBAL"] = "Attributes not consistent with each others: $res";
  483. }
  484. return $aResults;
  485. }
  486. protected function CreateObject(&$aResult, $iRow, $aRowData, CMDBChange $oChange = null)
  487. {
  488. $oTargetObj = MetaModel::NewObject($this->m_sClass);
  489. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  490. if (count($aErrors) > 0)
  491. {
  492. $sErrors = implode(', ', $aErrors);
  493. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  494. return $oTargetObj;
  495. }
  496. // Check that any external key will have a value proposed
  497. $aMissingKeys = array();
  498. foreach (MetaModel::GetExternalKeys($this->m_sClass) as $sExtKeyAttCode => $oExtKey)
  499. {
  500. if (!$oExtKey->IsNullAllowed())
  501. {
  502. if (!array_key_exists($sExtKeyAttCode, $this->m_aExtKeys) && !array_key_exists($sExtKeyAttCode, $this->m_aAttList))
  503. {
  504. $aMissingKeys[] = $oExtKey->GetLabel();
  505. }
  506. }
  507. }
  508. if (count($aMissingKeys) > 0)
  509. {
  510. $sMissingKeys = implode(', ', $aMissingKeys);
  511. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Could not be created, due to missing external key(s): $sMissingKeys");
  512. return $oTargetObj;
  513. }
  514. // Optionaly record the results
  515. //
  516. if ($oChange)
  517. {
  518. $newID = $oTargetObj->DBInsertTrackedNoReload($oChange);
  519. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj($this->m_sClass, $newID);
  520. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  521. $aResult[$iRow]["id"] = new CellStatus_Void($newID);
  522. }
  523. else
  524. {
  525. $aResult[$iRow]["__STATUS__"] = new RowStatus_NewObj();
  526. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  527. $aResult[$iRow]["id"] = new CellStatus_Void(0);
  528. }
  529. return $oTargetObj;
  530. }
  531. protected function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, CMDBChange $oChange = null)
  532. {
  533. $aResult[$iRow] = $this->PrepareObject($oTargetObj, $aRowData, $aErrors);
  534. // Reporting
  535. //
  536. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  537. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  538. if (count($aErrors) > 0)
  539. {
  540. $sErrors = implode(', ', $aErrors);
  541. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  542. return;
  543. }
  544. $aChangedFields = $oTargetObj->ListChanges();
  545. if (count($aChangedFields) > 0)
  546. {
  547. $aResult[$iRow]["__STATUS__"] = new RowStatus_Modify(count($aChangedFields));
  548. // Optionaly record the results
  549. //
  550. if ($oChange)
  551. {
  552. try
  553. {
  554. $oTargetObj->DBUpdateTracked($oChange);
  555. }
  556. catch(CoreException $e)
  557. {
  558. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage());
  559. }
  560. }
  561. }
  562. else
  563. {
  564. $aResult[$iRow]["__STATUS__"] = new RowStatus_NoChange();
  565. }
  566. }
  567. protected function UpdateMissingObject(&$aResult, $iRow, $oTargetObj, CMDBChange $oChange = null)
  568. {
  569. $aResult[$iRow] = $this->PrepareMissingObject($oTargetObj, $aErrors);
  570. // Reporting
  571. //
  572. $aResult[$iRow]["finalclass"] = get_class($oTargetObj);
  573. $aResult[$iRow]["id"] = new CellStatus_Void($oTargetObj->GetKey());
  574. if (count($aErrors) > 0)
  575. {
  576. $sErrors = implode(', ', $aErrors);
  577. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue("Unexpected attribute value(s)");
  578. return;
  579. }
  580. $aChangedFields = $oTargetObj->ListChanges();
  581. if (count($aChangedFields) > 0)
  582. {
  583. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(count($aChangedFields));
  584. // Optionaly record the results
  585. //
  586. if ($oChange)
  587. {
  588. try
  589. {
  590. $oTargetObj->DBUpdateTracked($oChange);
  591. }
  592. catch(CoreException $e)
  593. {
  594. $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue($e->getMessage());
  595. }
  596. }
  597. }
  598. else
  599. {
  600. $aResult[$iRow]["__STATUS__"] = new RowStatus_Disappeared(0);
  601. }
  602. }
  603. public function Process(CMDBChange $oChange = null)
  604. {
  605. // Note: $oChange can be null, in which case the aim is to check what would be done
  606. // Debug...
  607. //
  608. if (false)
  609. {
  610. echo "<pre>\n";
  611. echo "Attributes:\n";
  612. print_r($this->m_aAttList);
  613. echo "ExtKeys:\n";
  614. print_r($this->m_aExtKeys);
  615. echo "Reconciliation:\n";
  616. print_r($this->m_aReconcilKeys);
  617. echo "Synchro scope:\n";
  618. print_r($this->m_sSynchroScope);
  619. echo "Synchro changes:\n";
  620. print_r($this->m_aOnDisappear);
  621. //echo "Data:\n";
  622. //print_r($this->m_aData);
  623. echo "</pre>\n";
  624. exit;
  625. }
  626. $aResult = array();
  627. if (!is_null($this->m_sDateFormat) && (strlen($this->m_sDateFormat) > 0))
  628. {
  629. // Translate dates from the source data
  630. //
  631. foreach ($this->m_aAttList as $sAttCode => $iCol)
  632. {
  633. $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  634. if ($oAttDef instanceof AttributeDateTime)
  635. {
  636. foreach($this->m_aData as $iRow => $aRowData)
  637. {
  638. $sNewDate = utils::StringToTime($this->m_aData[$iRow][$iCol], $this->m_sDateFormat);
  639. if ($sNewDate !== false)
  640. {
  641. // Todo - improve the reporting
  642. $this->m_aData[$iRow][$iCol] = $sNewDate;
  643. }
  644. else
  645. {
  646. // Leave the cell unchanged
  647. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("wrong date format");
  648. $aResult[$iRow][$sAttCode] = new CellStatus_Issue(null, $this->m_aData[$iRow][$iCol], 'Wrong date format');
  649. }
  650. }
  651. }
  652. }
  653. }
  654. // Compute the results
  655. //
  656. if (!is_null($this->m_sSynchroScope))
  657. {
  658. $aVisited = array();
  659. }
  660. foreach($this->m_aData as $iRow => $aRowData)
  661. {
  662. if (isset($aResult[$iRow]["__STATUS__"]))
  663. {
  664. // An issue at the earlier steps - skip the rest
  665. continue;
  666. }
  667. $oReconciliationFilter = new CMDBSearchFilter($this->m_sClass);
  668. $bSkipQuery = false;
  669. foreach($this->m_aReconcilKeys as $sAttCode)
  670. {
  671. $valuecondition = null;
  672. if (array_key_exists($sAttCode, $this->m_aExtKeys))
  673. {
  674. if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
  675. {
  676. $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
  677. if ($oExtKey->IsNullAllowed())
  678. {
  679. $valuecondition = $oExtKey->GetNullValue();
  680. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
  681. }
  682. else
  683. {
  684. $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
  685. }
  686. }
  687. else
  688. {
  689. // The value has to be found or verified
  690. list($sQuery, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
  691. if (count($aMatches) == 1)
  692. {
  693. $oRemoteObj = reset($aMatches); // first item
  694. $valuecondition = $oRemoteObj->GetKey();
  695. $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
  696. }
  697. elseif (count($aMatches) == 0)
  698. {
  699. $aResult[$iRow][$sAttCode] = new CellStatus_SearchIssue();
  700. }
  701. else
  702. {
  703. $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $sQuery);
  704. }
  705. }
  706. }
  707. else
  708. {
  709. // The value is given in the data row
  710. $iCol = $this->m_aAttList[$sAttCode];
  711. $valuecondition = $aRowData[$iCol];
  712. }
  713. if (is_null($valuecondition))
  714. {
  715. $bSkipQuery = true;
  716. }
  717. else
  718. {
  719. $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
  720. }
  721. }
  722. if ($bSkipQuery)
  723. {
  724. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("failed to reconcile");
  725. }
  726. else
  727. {
  728. $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
  729. switch($oReconciliationSet->Count())
  730. {
  731. case 0:
  732. $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
  733. // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
  734. $aVisited[] = $oTargetObj->GetKey();
  735. break;
  736. case 1:
  737. $oTargetObj = $oReconciliationSet->Fetch();
  738. $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
  739. // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
  740. if (!is_null($this->m_sSynchroScope))
  741. {
  742. $aVisited[] = $oTargetObj->GetKey();
  743. }
  744. break;
  745. default:
  746. // Found several matches, ambiguous
  747. $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue("ambiguous reconciliation");
  748. $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->ToOql());
  749. $aResult[$iRow]["finalclass"]= 'n/a';
  750. }
  751. }
  752. }
  753. if (!is_null($this->m_sSynchroScope))
  754. {
  755. // Compute the delta between the scope and visited objects
  756. $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
  757. $oScopeSet = new DBObjectSet($oScopeSearch);
  758. while ($oObj = $oScopeSet->Fetch())
  759. {
  760. $iObj = $oObj->GetKey();
  761. if (!in_array($iObj, $aVisited))
  762. {
  763. $iRow++;
  764. $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
  765. }
  766. }
  767. }
  768. // Fill in the blanks - the result matrix is expected to be 100% complete
  769. //
  770. foreach($this->m_aData as $iRow => $aRowData)
  771. {
  772. foreach($this->m_aAttList as $iCol)
  773. {
  774. if (!array_key_exists($iCol, $aResult[$iRow]))
  775. {
  776. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  777. }
  778. }
  779. foreach($this->m_aExtKeys as $sAttCode => $aForeignAtts)
  780. {
  781. if (!array_key_exists($sAttCode, $aResult[$iRow]))
  782. {
  783. $aResult[$iRow][$sAttCode] = new CellStatus_Void('n/a');
  784. }
  785. foreach ($aForeignAtts as $sForeignAttCode => $iCol)
  786. {
  787. if (!array_key_exists($iCol, $aResult[$iRow]))
  788. {
  789. // The foreign attribute is one of our reconciliation key
  790. $aResult[$iRow][$iCol] = new CellStatus_Void($aRowData[$iCol]);
  791. }
  792. }
  793. }
  794. }
  795. return $aResult;
  796. }
  797. /**
  798. * Display the history of bulk imports
  799. */
  800. static function DisplayImportHistory(WebPage $oPage, $bFromAjax = false, $bShowAll = false)
  801. {
  802. $sAjaxDivId = "CSVImportHistory";
  803. if (!$bFromAjax)
  804. {
  805. $oPage->add('<div id="'.$sAjaxDivId.'">');
  806. }
  807. $oPage->p(Dict::S('UI:History:BulkImports+'));
  808. $oBulkChangeSearch = DBObjectSearch::FromOQL("SELECT CMDBChange WHERE userinfo LIKE '%(CSV)'");
  809. $iQueryLimit = $bShowAll ? 0 : MetaModel::GetConfig()->GetMaxDisplayLimit() + 1;
  810. $oBulkChanges = new DBObjectSet($oBulkChangeSearch, array('date' => false), array(), null, $iQueryLimit);
  811. $oAppContext = new ApplicationContext();
  812. $bLimitExceeded = false;
  813. if ($oBulkChanges->Count() > MetaModel::GetConfig()->GetMaxDisplayLimit())
  814. {
  815. $bLimitExceeded = true;
  816. if (!$bShowAll)
  817. {
  818. $iMaxObjects = MetaModel::GetConfig()->GetMinDisplayLimit();
  819. $oBulkChanges->SetLimit($iMaxObjects);
  820. }
  821. }
  822. $oBulkChanges->Seek(0);
  823. $aDetails = array();
  824. while ($oChange = $oBulkChanges->Fetch())
  825. {
  826. $sDate = '<a href="?step=10&changeid='.$oChange->GetKey().'&'.$oAppContext->GetForLink().'">'.$oChange->Get('date').'</a>';
  827. $sUser = $oChange->GetUserName();
  828. if (preg_match('/^(.*)\\(CSV\\)$/i', $oChange->Get('userinfo'), $aMatches))
  829. {
  830. $sUser = $aMatches[1];
  831. }
  832. else
  833. {
  834. $sUser = $oChange->Get('userinfo');
  835. }
  836. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpCreate WHERE change = :change_id");
  837. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  838. $iCreated = $oOpSet->Count();
  839. // Get the class from the first item found (assumption: a CSV load is done for a single class)
  840. if ($oCreateOp = $oOpSet->Fetch())
  841. {
  842. $sClass = $oCreateOp->Get('objclass');
  843. }
  844. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOpSetAttribute WHERE change = :change_id");
  845. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $oChange->GetKey()));
  846. $aModified = array();
  847. $aAttList = array();
  848. while ($oModified = $oOpSet->Fetch())
  849. {
  850. // Get the class (if not done earlier on object creation)
  851. $sClass = $oModified->Get('objclass');
  852. $iKey = $oModified->Get('objkey');
  853. $sAttCode = $oModified->Get('attcode');
  854. $aAttList[$sClass][$sAttCode] = true;
  855. $aModified["$sClass::$iKey"] = true;
  856. }
  857. $iModified = count($aModified);
  858. // Assumption: there is only one class of objects being loaded
  859. // Then the last class found gives us the class for every object
  860. if ( ($iModified > 0) || ($iCreated > 0))
  861. {
  862. $aDetails[] = array('date' => $sDate, 'user' => $sUser, 'class' => $sClass, 'created' => $iCreated, 'modified' => $iModified);
  863. }
  864. }
  865. $aConfig = array( 'date' => array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')),
  866. 'user' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')),
  867. 'class' => array('label' => Dict::S('Core:AttributeClass'), 'description' => Dict::S('Core:AttributeClass+')),
  868. 'created' => array('label' => Dict::S('UI:History:StatsCreations'), 'description' => Dict::S('UI:History:StatsCreations+')),
  869. 'modified' => array('label' => Dict::S('UI:History:StatsModifs'), 'description' => Dict::S('UI:History:StatsModifs+')),
  870. );
  871. if ($bLimitExceeded)
  872. {
  873. if ($bShowAll)
  874. {
  875. // Collapsible list
  876. $oPage->add('<p>'.Dict::Format('UI:CountOfResults', $oBulkChanges->Count()).'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(false);">'.Dict::S('UI:CollapseList').'</a></p>');
  877. }
  878. else
  879. {
  880. // Truncated list
  881. $iMinDisplayLimit = MetaModel::GetConfig()->GetMinDisplayLimit();
  882. $sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oBulkChanges->Count());
  883. $sLinkLabel = Dict::S('UI:DisplayAll');
  884. $oPage->add('<p>'.$sCollapsedLabel.'&nbsp;&nbsp;<a class="truncated" onclick="OnTruncatedHistoryToggle(true);">'.$sLinkLabel.'</p>');
  885. $oPage->add_ready_script(
  886. <<<EOF
  887. $('#$sAjaxDivId table.listResults').addClass('truncated');
  888. $('#$sAjaxDivId table.listResults tr:last td').addClass('truncated');
  889. EOF
  890. );
  891. $sAppContext = $oAppContext->GetForLink();
  892. $oPage->add_script(
  893. <<<EOF
  894. function OnTruncatedHistoryToggle(bShowAll)
  895. {
  896. $.get(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?{$sAppContext}', {operation: 'displayCSVHistory', showall: bShowAll}, function(data)
  897. {
  898. $('#$sAjaxDivId').html(data);
  899. var table = $('#$sAjaxDivId .listResults');
  900. table.tableHover(); // hover tables
  901. table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
  902. }
  903. );
  904. }
  905. EOF
  906. );
  907. }
  908. }
  909. else
  910. {
  911. // Normal display - full list without any decoration
  912. }
  913. $oPage->table($aConfig, $aDetails);
  914. if (!$bFromAjax)
  915. {
  916. $oPage->add('</div>');
  917. }
  918. }
  919. /**
  920. * Display the details of an import
  921. */
  922. static function DisplayImportHistoryDetails(iTopWebPage $oPage, $iChange)
  923. {
  924. if ($iChange == 0)
  925. {
  926. throw new Exception("Missing parameter changeid");
  927. }
  928. $oChange = MetaModel::GetObject('CMDBChange', $iChange, false);
  929. if (is_null($oChange))
  930. {
  931. throw new Exception("Unknown change: $iChange");
  932. }
  933. $oPage->add("<div><p><h1>".Dict::Format('UI:History:BulkImportDetails', $oChange->Get('date'), $oChange->GetUserName())."</h1></p></div>\n");
  934. // Assumption : change made one single class of objects
  935. $aObjects = array();
  936. $aAttributes = array(); // array of attcode => occurences
  937. $oOpSearch = DBObjectSearch::FromOQL("SELECT CMDBChangeOp WHERE change = :change_id");
  938. $oOpSet = new DBObjectSet($oOpSearch, array(), array('change_id' => $iChange));
  939. while ($oOperation = $oOpSet->Fetch())
  940. {
  941. $sClass = $oOperation->Get('objclass');
  942. $iKey = $oOperation->Get('objkey');
  943. $iObjId = "$sClass::$iKey";
  944. if (!isset($aObjects[$iObjId]))
  945. {
  946. $aObjects[$iObjId] = array();
  947. $aObjects[$iObjId]['__class__'] = $sClass;
  948. $aObjects[$iObjId]['__id__'] = $iKey;
  949. }
  950. if (get_class($oOperation) == 'CMDBChangeOpCreate')
  951. {
  952. $aObjects[$iObjId]['__created__'] = true;
  953. }
  954. elseif ($oOperation instanceof CMDBChangeOpSetAttribute)
  955. {
  956. $sAttCode = $oOperation->Get('attcode');
  957. if (get_class($oOperation) == 'CMDBChangeOpSetAttributeScalar')
  958. {
  959. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  960. if ($oAttDef->IsExternalKey())
  961. {
  962. $oOldTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('oldvalue'));
  963. $oNewTarget = MetaModel::GetObject($oAttDef->GetTargetClass(), $oOperation->Get('newvalue'));
  964. $sOldValue = $oOldTarget->GetHyperlink();
  965. $sNewValue = $oNewTarget->GetHyperlink();
  966. }
  967. else
  968. {
  969. $sOldValue = $oOperation->GetAsHTML('oldvalue');
  970. $sNewValue = $oOperation->GetAsHTML('newvalue');
  971. }
  972. $aObjects[$iObjId][$sAttCode] = $sOldValue.' -&gt; '.$sNewValue;
  973. }
  974. else
  975. {
  976. $aObjects[$iObjId][$sAttCode] = 'n/a';
  977. }
  978. if (isset($aAttributes[$sAttCode]))
  979. {
  980. $aAttributes[$sAttCode]++;
  981. }
  982. else
  983. {
  984. $aAttributes[$sAttCode] = 1;
  985. }
  986. }
  987. }
  988. $aDetails = array();
  989. foreach($aObjects as $iUId => $aObjData)
  990. {
  991. $aRow = array();
  992. $oObject = MetaModel::GetObject($aObjData['__class__'], $aObjData['__id__'], false);
  993. if (is_null($oObject))
  994. {
  995. $aRow['object'] = $aObjData['__class__'].'::'.$aObjData['__id__'].' (deleted)';
  996. }
  997. else
  998. {
  999. $aRow['object'] = $oObject->GetHyperlink();
  1000. }
  1001. if (isset($aObjData['__created__']))
  1002. {
  1003. $aRow['operation'] = Dict::S('Change:ObjectCreated');
  1004. }
  1005. else
  1006. {
  1007. $aRow['operation'] = Dict::S('Change:ObjectModified');
  1008. }
  1009. foreach ($aAttributes as $sAttCode => $iOccurences)
  1010. {
  1011. if (isset($aObjData[$sAttCode]))
  1012. {
  1013. $aRow[$sAttCode] = $aObjData[$sAttCode];
  1014. }
  1015. elseif (!is_null($oObject))
  1016. {
  1017. // This is the current vaslue: $oObject->GetAsHtml($sAttCode)
  1018. // whereas we are displaying the value that was set at the time
  1019. // the object was created
  1020. // This requires addtional coding...let's do that later
  1021. $aRow[$sAttCode] = '';
  1022. }
  1023. else
  1024. {
  1025. $aRow[$sAttCode] = '';
  1026. }
  1027. }
  1028. $aDetails[] = $aRow;
  1029. }
  1030. $aConfig = array();
  1031. $aConfig['object'] = array('label' => MetaModel::GetName($sClass), 'description' => MetaModel::GetClassDescription($sClass));
  1032. $aConfig['operation'] = array('label' => Dict::S('UI:History:Changes'), 'description' => Dict::S('UI:History:Changes+'));
  1033. foreach ($aAttributes as $sAttCode => $iOccurences)
  1034. {
  1035. $aConfig[$sAttCode] = array('label' => MetaModel::GetLabel($sClass, $sAttCode), 'description' => MetaModel::GetDescription($sClass, $sAttCode));
  1036. }
  1037. $oPage->table($aConfig, $aDetails);
  1038. }
  1039. /**
  1040. * Get the user friendly name for an 'extended' attribute code i.e 'name', becomes 'Name' and 'org_id->name' becomes 'Organization->Name'
  1041. * @param string $sClassName The name of the class
  1042. * @param string $sAttCodeEx Either an attribute code or ext_key_name->att_code
  1043. * @return string A user friendly format of the string: AttributeName or AttributeName->ExtAttributeName
  1044. */
  1045. public static function GetFriendlyAttCodeName($sClassName, $sAttCodeEx)
  1046. {
  1047. $sFriendlyName = '';
  1048. if (preg_match('/(.+)->(.+)/', $sAttCodeEx, $aMatches) > 0)
  1049. {
  1050. $sAttribute = $aMatches[1];
  1051. $sField = $aMatches[2];
  1052. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttribute);
  1053. if ($oAttDef->IsExternalKey())
  1054. {
  1055. $sTargetClass = $oAttDef->GetTargetClass();
  1056. $oTargetAttDef = MetaModel::GetAttributeDef($sTargetClass, $sField);
  1057. $sFriendlyName = $oAttDef->GetLabel().'->'.$oTargetAttDef->GetLabel();
  1058. }
  1059. else
  1060. {
  1061. // hum, hum... should never happen, we'd better raise an exception
  1062. throw(new Exception(Dict::Format('UI:CSVImport:ErrorExtendedAttCode', $sAttCodeEx, $sAttribute, $sClassName)));
  1063. }
  1064. }
  1065. else
  1066. {
  1067. if ($sAttCodeEx == 'id')
  1068. {
  1069. $sFriendlyName = Dict::S('UI:CSVImport:idField');
  1070. }
  1071. else
  1072. {
  1073. $oAttDef = MetaModel::GetAttributeDef($sClassName, $sAttCodeEx);
  1074. $sFriendlyName = $oAttDef->GetLabel();
  1075. }
  1076. }
  1077. return $sFriendlyName;
  1078. }
  1079. }
  1080. ?>