bulkchange.class.inc.php 34 KB

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