synchrodatasource.class.inc.php 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  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. * Data Exchange - synchronization with external applications (incoming data)
  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. class SynchroDataSource extends cmdbAbstractObject
  25. {
  26. public static function Init()
  27. {
  28. $aParams = array
  29. (
  30. "category" => "core/cmdb,view_in_gui",
  31. "key_type" => "autoincrement",
  32. "name_attcode" => array('name'),
  33. "state_attcode" => "",
  34. "reconc_keys" => array(),
  35. "db_table" => "priv_sync_datasource",
  36. "db_key_field" => "id",
  37. "db_finalclass_field" => "realclass",
  38. "display_template" => "",
  39. "icon" => "../images/synchro.png",
  40. );
  41. MetaModel::Init_Params($aParams);
  42. //MetaModel::Init_InheritAttributes();
  43. MetaModel::Init_AddAttribute(new AttributeString("name", array("allowed_values"=>null, "sql"=>"name", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  44. MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  45. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('implementation,production,obsolete'), "sql"=>"status", "default_value"=>"implementation", "is_null_allowed"=>false, "depends_on"=>array())));
  46. MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=>null, "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array())));
  47. MetaModel::Init_AddAttribute(new AttributeClass("scope_class", array("class_category"=>"bizmodel", "more_values"=>"", "sql"=>"scope_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  48. MetaModel::Init_AddAttribute(new AttributeString("scope_restriction", array("allowed_values"=>null, "sql"=>"scope_restriction", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  49. //MetaModel::Init_AddAttribute(new AttributeDateTime("last_synchro_date", array("allowed_values"=>null, "sql"=>"last_synchro_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
  50. // Format: '1 hour', '2 weeks', '3 hoursABCDEF'... Cf DateTime->Modify()
  51. MetaModel::Init_AddAttribute(new AttributeString("full_load_periodicity", array("allowed_values"=>null, "sql"=>"full_load_periodicity", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  52. // MetaModel::Init_AddAttribute(new AttributeString("reconciliation_list", array("allowed_values"=>null, "sql"=>"reconciliation_list", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  53. MetaModel::Init_AddAttribute(new AttributeEnum("reconciliation_policy", array("allowed_values"=>new ValueSetEnum('use_primary_key,use_attributes'), "sql"=>"reconciliation_policy", "default_value"=>"use_attributes", "is_null_allowed"=>false, "depends_on"=>array())));
  54. MetaModel::Init_AddAttribute(new AttributeEnum("action_on_zero", array("allowed_values"=>new ValueSetEnum('create,error'), "sql"=>"action_on_zero", "default_value"=>"create", "is_null_allowed"=>false, "depends_on"=>array())));
  55. MetaModel::Init_AddAttribute(new AttributeEnum("action_on_one", array("allowed_values"=>new ValueSetEnum('update,error,delete'), "sql"=>"action_on_one", "default_value"=>"update", "is_null_allowed"=>false, "depends_on"=>array())));
  56. MetaModel::Init_AddAttribute(new AttributeEnum("action_on_multiple", array("allowed_values"=>new ValueSetEnum('take_first,create,error'), "sql"=>"action_on_multiple", "default_value"=>"error", "is_null_allowed"=>false, "depends_on"=>array())));
  57. MetaModel::Init_AddAttribute(new AttributeEnum("delete_policy", array("allowed_values"=>new ValueSetEnum('ignore,delete,update,update_then_delete'), "sql"=>"delete_policy", "default_value"=>"ignore", "is_null_allowed"=>false, "depends_on"=>array())));
  58. MetaModel::Init_AddAttribute(new AttributeString("delete_policy_update", array("allowed_values"=>null, "sql"=>"delete_policy_update", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  59. // Format: '1 hour', '2 weeks', '3 hoursABCDEF'... Cf DateTime->Modify()
  60. MetaModel::Init_AddAttribute(new AttributeString("delete_policy_retention", array("allowed_values"=>null, "sql"=>"delete_policy_retention", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  61. MetaModel::Init_AddAttribute(new AttributeLinkedSet("attribute_list", array("linked_class"=>"SynchroAttribute", "ext_key_to_me"=>"sync_source_id", "allowed_values"=>null, "count_min"=>0, "count_max"=>0, "depends_on"=>array())));
  62. // Display lists
  63. MetaModel::Init_SetZListItems('details', array('name', 'description', 'scope_class', 'scope_restriction', 'status', 'user_id', 'full_load_periodicity', 'reconciliation_policy', 'action_on_zero', 'action_on_one', 'action_on_multiple', 'delete_policy', 'delete_policy_update', 'delete_policy_retention', /*'attribute_list'*/)); // Attributes to be displayed for the complete details
  64. MetaModel::Init_SetZListItems('list', array('scope_class', 'status', 'user_id', 'full_load_periodicity')); // Attributes to be displayed for a list
  65. // Search criteria
  66. MetaModel::Init_SetZListItems('standard_search', array('name', 'status', 'scope_class', 'user_id')); // Criteria of the std search form
  67. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  68. }
  69. public function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
  70. {
  71. if (!$this->IsNew())
  72. {
  73. $oPage->SetCurrentTab(Dict::S('Core:SynchroAttributes'));
  74. $oAttributeSet = $this->Get('attribute_list');
  75. $aAttributes = array();
  76. while($oAttribute = $oAttributeSet->Fetch())
  77. {
  78. $aAttributes[$oAttribute->Get('attcode')] = $oAttribute;
  79. }
  80. $aAttribs = array(
  81. 'attcode' => array('label'=>'Attribute', 'description' => 'Field of the object'),
  82. 'reconciliation' => array('label'=>'Reconciliation ?', 'description' => 'Used for searching'),
  83. 'update' => array('label'=>'Update ?', 'description' => 'Used to update the object'),
  84. 'update_policy' => array('label'=>'Update Policy', 'description' => 'Behavior of the updated field'),
  85. );
  86. $aValues = array();
  87. foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
  88. {
  89. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  90. {
  91. if (isset($aAttributes[$sAttCode]))
  92. {
  93. $oAttribute = $aAttributes[$sAttCode];
  94. }
  95. else
  96. {
  97. $oAttribute = new SynchroAttribute();
  98. $oAttribute->Set('sync_source_id', $this->GetKey());
  99. $oAttribute->Set('attcode', $sAttCode);
  100. $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
  101. $oAttribute->Set('update', 1);
  102. $oAttribute->Set('update_policy', 'master_locked');
  103. }
  104. if (!$bEditMode)
  105. {
  106. // Read-only mode
  107. $aRow['reconciliation'] = $oAttribute->Get('reconcile') == 1 ? Dict::S('UI:Synchro:Yes') : Dict::S('UI:Synchro:No');
  108. $aRow['update'] = $oAttribute->Get('update') == 1 ? Dict::S('UI:Synchro:Yes') : Dict::S('UI:Synchro:No');
  109. $aRow['attcode'] = MetaModel::GetLabel($this->GetTargetClass(), $oAttribute->Get('attcode'));
  110. $aRow['update_policy'] = $oAttribute->GetAsHTML('update_policy');
  111. }
  112. else
  113. {
  114. // Read-only mode
  115. $sAttCode = $oAttribute->Get('attcode');
  116. $sChecked = $oAttribute->Get('reconcile') == 1 ? 'checked' : '';
  117. $aRow['reconciliation'] = "<input type=\"checkbox\" name=\"reconciliation[$sAttCode]\" $sChecked/>";
  118. $sChecked = $oAttribute->Get('update') == 1 ? 'checked' : '';
  119. $aRow['update'] = "<input type=\"checkbox\" name=\"update[$sAttCode]\" $sChecked/>";
  120. $aRow['attcode'] = MetaModel::GetLabel($this->GetTargetClass(), $oAttribute->Get('attcode'));
  121. $oAttDef = MetaModel::GetAttributeDef(get_class($oAttribute), 'update_policy');
  122. $aRow['update_policy'] = cmdbAbstractObject::GetFormElementForField($oPage, get_class($oAttribute), 'update_policy', $oAttDef, $oAttribute->Get('update_policy'), '', 'update_policy_'.$sAttCode, "[$sAttCode]");
  123. }
  124. $aValues[] = $aRow;
  125. }
  126. }
  127. $oPage->Table($aAttribs, $aValues);
  128. }
  129. parent::DisplayBareRelations($oPage, $bEditMode);
  130. }
  131. public function GetAttributeFlags($sAttCode)
  132. {
  133. if (($sAttCode == 'scope_class') && (!$this->IsNew()))
  134. {
  135. return OPT_ATT_READONLY;
  136. }
  137. return parent::GetAttributeFlags($sAttCode);
  138. }
  139. public function UpdateObject($sFormPrefix = '')
  140. {
  141. parent::UpdateObject($sFormPrefix);
  142. // And now read the other post parameters...
  143. $oAttributeSet = $this->Get('attribute_list');
  144. $aAttributes = array();
  145. while($oAttribute = $oAttributeSet->Fetch())
  146. {
  147. $aAttributes[$oAttribute->Get('attcode')] = $oAttribute;
  148. }
  149. $aReconcile = utils::ReadPostedParam('reconciliation', array());
  150. $aUpdate = utils::ReadPostedParam('update', array());
  151. $aUpdatePolicy = utils::ReadPostedParam('attr_update_policy', array());
  152. // update_policy cannot be empty, so there is one entry per attribute, use this to iterate
  153. // through all the writable attributes
  154. foreach($aUpdatePolicy as $sAttCode => $sValue)
  155. {
  156. if(!isset($aAttributes[$sAttCode]))
  157. {
  158. $oAttribute = new SynchroAttribute();
  159. $oAttribute->Set('sync_source_id', $this->GetKey());
  160. $oAttribute->Set('attcode', $sAttCode);
  161. }
  162. else
  163. {
  164. $oAttribute = $aAttributes[$sAttCode];
  165. }
  166. $bReconcile = 0;
  167. if (isset($aReconcile[$sAttCode]))
  168. {
  169. $bReconcile = $aReconcile[$sAttCode] == 'on' ? 1 : 0;
  170. }
  171. $bUpdate = 0 ; // Default / initial value
  172. if (isset($aUpdate[$sAttCode]))
  173. {
  174. $bUpdate = $aUpdate[$sAttCode] == 'on' ? 1 : 0;
  175. }
  176. $oAttribute->Set('reconcile', $bReconcile);
  177. $oAttribute->Set('update', $bUpdate);
  178. $oAttribute->Set('update_policy', $sValue);
  179. $oAttributeSet->AddObject($oAttribute);
  180. }
  181. $this->Set('attribute_list', $oAttributeSet);
  182. }
  183. public function GetTargetClass()
  184. {
  185. return $this->Get('scope_class');
  186. }
  187. public function GetDataTable()
  188. {
  189. $sName = strtolower($this->GetTargetClass());
  190. $sName = str_replace('\'"&@|\\/ ', '_', $sName); // Remove forbidden characters from the table name
  191. $sName .= '_'.$this->GetKey(); // Add a suffix for unicity
  192. $sTable = MetaModel::GetConfig()->GetDBSubName()."synchro_data_$sName"; // Add the prefix if any
  193. return $sTable;
  194. }
  195. /**
  196. * When inserting a new datasource object, also create the SynchroAttribute objects
  197. * for each field of the target class
  198. */
  199. protected function OnInsert()
  200. {
  201. // Create all the SynchroAttribute records
  202. $oAttributeSet = $this->Get('attribute_list');
  203. foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
  204. {
  205. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  206. {
  207. $oAttribute = new SynchroAttribute();
  208. $oAttribute->Set('sync_source_id', $this->GetKey());
  209. $oAttribute->Set('attcode', $sAttCode);
  210. $oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
  211. $oAttribute->Set('update', 1);
  212. $oAttribute->Set('update_policy', 'master_locked');
  213. $oAttributeSet->AddObject($oAttribute);
  214. }
  215. }
  216. $this->Set('attribute_list', $oAttributeSet);
  217. }
  218. /**
  219. * When the new datasource has been created, let's create the synchro_data table
  220. * that will hold the data records and the correspoding triggers which will maintain
  221. * both tables in sync
  222. */
  223. protected function AfterInsert()
  224. {
  225. parent::AfterInsert();
  226. $sTable = $this->GetDataTable();
  227. $aColumns = $this->GetSQLColumns();
  228. $aFieldDefs = array();
  229. // Allow '0', otherwise mysql will render an error when the id is not given
  230. // (the trigger is expected to set the value, but it is not executed soon enough)
  231. $aFieldDefs[] = "id INTEGER(11) NOT NULL DEFAULT 0 ";
  232. $aFieldDefs[] = "`primary_key` VARCHAR(255) NULL DEFAULT NULL";
  233. foreach($aColumns as $sColumn => $ColSpec)
  234. {
  235. $aFieldDefs[] = "`$sColumn` $ColSpec NULL DEFAULT NULL";
  236. }
  237. $aFieldDefs[] = "INDEX (id)";
  238. $aFieldDefs[] = "INDEX (primary_key)";
  239. $sFieldDefs = implode(', ', $aFieldDefs);
  240. $sCreateTable = "CREATE TABLE `$sTable` ($sFieldDefs) ENGINE = innodb;";
  241. CMDBSource::Query($sCreateTable);
  242. $sTriggerInsert = "CREATE TRIGGER `{$sTable}_bi` BEFORE INSERT ON $sTable";
  243. $sTriggerInsert .= " FOR EACH ROW";
  244. $sTriggerInsert .= " BEGIN";
  245. $sTriggerInsert .= " INSERT INTO priv_sync_replica (sync_source_id, status_last_seen, `status`) VALUES ({$this->GetKey()}, NOW(), 'new');";
  246. $sTriggerInsert .= " SET NEW.id = LAST_INSERT_ID();";
  247. $sTriggerInsert .= " END;";
  248. CMDBSource::Query($sTriggerInsert);
  249. $aModified = array();
  250. foreach($aColumns as $sColumn => $ColSpec)
  251. {
  252. // <=> is a null-safe 'EQUALS' operator (there is no equivalent for "DIFFERS FROM")
  253. $aModified[] = "NOT(NEW.`$sColumn` <=> OLD.`$sColumn`)";
  254. }
  255. $sIsModified = '('.implode(') OR (', $aModified).')';
  256. // Update the replica
  257. //
  258. // status is forced to "new" if the replica was obsoleted directly from the state "new" (dest_id = null)
  259. // otherwise, if status was either 'obsolete' or 'synchronized' it is turned into 'modified' or 'synchronized' depending on the changes
  260. // otherwise, the status is left as is
  261. $sTriggerUpdate = "CREATE TRIGGER `{$sTable}_bu` BEFORE UPDATE ON $sTable";
  262. $sTriggerUpdate .= " FOR EACH ROW";
  263. $sTriggerUpdate .= " BEGIN";
  264. $sTriggerUpdate .= " IF @itopuser is null THEN";
  265. $sTriggerUpdate .= " UPDATE priv_sync_replica SET status_last_seen = NOW(), `status` = IF(`status` = 'obsolete', IF(`dest_id` IS NULL, 'new', 'modified'), IF(`status` IN ('synchronized') AND ($sIsModified), 'modified', `status`)) WHERE sync_source_id = {$this->GetKey()} AND id = OLD.id;";
  266. $sTriggerUpdate .= " SET NEW.id = OLD.id;"; // make sure this id won't change
  267. $sTriggerUpdate .= " END IF;";
  268. $sTriggerUpdate .= " END;";
  269. CMDBSource::Query($sTriggerUpdate);
  270. $sTriggerInsert = "CREATE TRIGGER `{$sTable}_ad` AFTER DELETE ON $sTable";
  271. $sTriggerInsert .= " FOR EACH ROW";
  272. $sTriggerInsert .= " BEGIN";
  273. $sTriggerInsert .= " DELETE FROM priv_sync_replica WHERE id = OLD.id;";
  274. $sTriggerInsert .= " END;";
  275. CMDBSource::Query($sTriggerInsert);
  276. }
  277. protected function AfterDelete()
  278. {
  279. parent::AfterDelete();
  280. $sTable = $this->GetDataTable();
  281. $sDropTable = "DROP TABLE `$sTable`";
  282. CMDBSource::Query($sDropTable);
  283. // TO DO - check that triggers get dropped with the table
  284. }
  285. /**
  286. * Perform a synchronization between the data stored in the replicas (&synchro_data_xxx_xx table)
  287. * and the iTop objects. If the lastFullLoadStartDate is NOT specified then the full_load_periodicity
  288. * is used to determine which records are obsolete.
  289. * @param Hash $aDataToReplica Debugs/Trace information, one entry per replica
  290. * @param DateTime $oLastFullLoadStartDate Date of the last full load (start date/time), if known
  291. * @return void
  292. */
  293. public function Synchronize(&$aDataToReplica, $oLastFullLoadStartDate = null)
  294. {
  295. // Create a change used for logging all the modifications/creations happening during the synchro
  296. $oMyChange = MetaModel::NewObject("CMDBChange");
  297. $oMyChange->Set("date", time());
  298. $sUserString = CMDBChange::GetCurrentUserName();
  299. $oMyChange->Set("userinfo", $sUserString);
  300. $iChangeId = $oMyChange->DBInsert();
  301. // Start logging this execution (stats + protection against reentrance)
  302. //
  303. $oStatLog = new SynchroLog();
  304. $oStatLog->Set('sync_source_id', $this->GetKey());
  305. $oStatLog->Set('start_date', time());
  306. $oStatLog->Set('status', 'running');
  307. $oStatLog->Set('stats_nb_seen', 0);
  308. $oStatLog->Set('stats_nb_modified', 0);
  309. $oStatLog->Set('stats_nb_errors', 0);
  310. $oStatLog->Set('stats_nb_created', 0);
  311. $oStatLog->Set('stats_nb_deleted', 0);
  312. $oStatLog->Set('stats_nb_reconciled', 0);
  313. $oStatLog->DBInsertTracked($oMyChange);
  314. try
  315. {
  316. $this->DoSynchronize($aDataToReplica, $oLastFullLoadStartDate, $oMyChange, $oStatLog);
  317. $oStatLog->Set('end_date', time());
  318. $oStatLog->Set('status', 'completed');
  319. $oStatLog->DBUpdateTracked($oMyChange);
  320. }
  321. catch (Exception $e)
  322. {
  323. $oStatLog->Set('end_date', time());
  324. $oStatLog->Set('status', 'completed');
  325. $oStatLog->DBUpdateTracked($oMyChange);
  326. }
  327. }
  328. protected function DoSynchronize(&$aDataToReplica, $oLastFullLoadStartDate, $oMyChange, &$oStatLog)
  329. {
  330. // Get all the replicas that were not seen in the last import and mark them as obsolete
  331. if ($oLastFullLoadStartDate == null)
  332. {
  333. // No previous import known, use the full_load_periodicity value... and the current date
  334. $oLastFullLoadStartDate = new DateTime(); // Now
  335. // TO DO: how do we support localization here ??
  336. $sLoadPeriodicity = trim($this->Get('full_load_periodicity'));
  337. if (strlen($sLoadPeriodicity) > 0)
  338. {
  339. $sInterval = '-'.$sLoadPeriodicity;
  340. // Note: the PHP doc states that Modify return FALSE in case of error
  341. // but, this is actually NOT the case
  342. // Therefore, I do compare before and after, considering that the
  343. // format is incorrect when the datetime remains unchanged
  344. $sBefore = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  345. $oLastFullLoadStartDate->Modify($sInterval);
  346. $sAfter = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  347. if ($sBefore == $sAfter)
  348. {
  349. throw new CoreException("Data exchange: Wrong interval specification", array('interval' => $sInterval, 'source_id' => $this->GetKey()));
  350. }
  351. }
  352. }
  353. $sLimitDate = $oLastFullLoadStartDate->Format('Y-m-d H:i:s');
  354. // TO DO: remove trace
  355. echo "<p>sLimitDate: $sLimitDate</p>\n";
  356. $sSelectToObsolete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen < :last_import";
  357. $oSetToObsolete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate));
  358. while($oReplica = $oSetToObsolete->Fetch())
  359. {
  360. // TO DO: take the appropriate action based on the 'delete_policy' field
  361. $sUpdateOnObsolete = $this->Get('delete_policy');
  362. if ( ($sUpdateOnObsolete == 'update') || ($sUpdateOnObsolete == 'update_then_delete') )
  363. {
  364. // TO DO: remove trace
  365. echo "<p>Destination object: (dest_id:".$oReplica->Get('dest_id').") to be updated.</p>";
  366. $aToUpdate = array();
  367. $aToUpdate = explode(';', $this->Get('delete_policy_update')); //ex: 'status:obsolete;description:stopped',
  368. foreach($aToUpdate as $sUpdateSpec)
  369. {
  370. $aUpdateSpec = explode(':', $sUpdateSpec);
  371. if (count($aUpdateSpec) == 2)
  372. {
  373. $sAttCode = $aUpdateSpec[0];
  374. $sValue = $aUpdateSpec[1];
  375. $aToUpdate[$sAttCode] = $sValue;
  376. }
  377. }
  378. $oReplica->UpdateDestObject($aToUpdate, $oMyChange, $oStatLog);
  379. }
  380. // TO DO: remove trace
  381. echo "<p>Replica id:".$oReplica->GetKey()." (dest_id:".$oReplica->Get('dest_id').") marked as obsolete</p>";
  382. $oReplica->Set('status', 'obsolete');
  383. $oReplica->DBUpdateTracked($oMyChange);
  384. }
  385. // Get all the replicas that are 'new' or modified
  386. //
  387. // Get the list of SQL columns
  388. $sClass = $this->GetTargetClass();
  389. // TO DO: remove trace
  390. echo "<p>TargetClass: $sClass</p>";
  391. $aAttCodes = array();
  392. $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND update = 1";
  393. $oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
  394. while ($oSyncAtt = $oSetAtt->Fetch())
  395. {
  396. $aAttCodes[] = $oSyncAtt->Get('attcode');
  397. }
  398. $aColumns = $this->GetSQLColumns($aAttCodes);
  399. $aExtDataFields = array_keys($aColumns);
  400. $aExtDataFields[] = 'primary_key';
  401. $aExtDataSpec = array(
  402. 'table' => $this->GetDataTable(),
  403. 'join_key' => 'id',
  404. 'fields' => $aExtDataFields
  405. );
  406. // Get the list of reconciliation keys
  407. $aReconciliationKeys = array();
  408. if ($this->Get('reconciliation_policy') == 'use_attributes')
  409. {
  410. $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND reconcile = 1";
  411. $oAttSet = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
  412. while ($oSyncAtt = $oAttSet->Fetch())
  413. {
  414. $aReconciliationKeys[] = $oSyncAtt->Get('attcode');
  415. }
  416. }
  417. elseif ($this->Get('reconciliation_policy') == 'use_primary_key')
  418. {
  419. $aReconciliationKeys[] = "primary_key";
  420. }
  421. // TO DO: remove trace
  422. echo "Reconciliation on: {".implode(', ', $aReconciliationKeys)."}<br/>\n";
  423. $aAttributes = array();
  424. foreach($aAttCodes as $sAttCode)
  425. {
  426. $oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
  427. if ($oAttDef->IsWritable() && $oAttDef->IsScalar())
  428. {
  429. $aAttributes[] = $sAttCode;
  430. }
  431. }
  432. $sSelectToSync = "SELECT SynchroReplica WHERE (status = 'new' OR status = 'modified') AND sync_source_id = :source_id";
  433. $oSetToSync = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, $aExtDataSpec, 0 /* limitCount */, 0 /* limitStart */);
  434. while($oReplica = $oSetToSync->Fetch())
  435. {
  436. $oStatLog->Set('stats_nb_seen', $oStatLog->Get('stats_nb_seen') + 1);
  437. if ($oReplica->Get('status') == 'modified')
  438. {
  439. $oStatLog->Set('stats_nb_modified', $oStatLog->Get('stats_nb_modified') + 1);
  440. }
  441. $oReplica->Synchro($this, $aReconciliationKeys, $aAttributes, $oMyChange, $oStatLog);
  442. }
  443. // Get all the replicas that are to be deleted
  444. //
  445. $oDeletionDate = $oLastFullLoadStartDate;
  446. $sDeleteRetention = trim($this->Get('delete_policy_retention'));
  447. if (strlen($sDeleteRetention) > 0)
  448. {
  449. $sInterval = '-'.$sDeleteRetention;
  450. // Note: the PHP doc states that Modify return FALSE in case of error
  451. // but, this is actually NOT the case
  452. // Therefore, I do compare before and after, considering that the
  453. // format is incorrect when the datetime remains unchanged
  454. $sBefore = $oDeletionDate->Format('Y-m-d H:i:s');
  455. $oDeletionDate->Modify($sInterval);
  456. $sAfter = $oDeletionDate->Format('Y-m-d H:i:s');
  457. if ($sBefore == $sAfter)
  458. {
  459. throw new CoreException("Data exchange: Wrong interval specification", array('interval' => $sInterval, 'source_id' => $this->GetKey()));
  460. }
  461. }
  462. $sDeletionDate = $oDeletionDate->Format('Y-m-d H:i:s');
  463. // TO DO: remove trace
  464. echo "<p>sDeletionDate: $sDeletionDate</p>\n";
  465. $sSelectToDelete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('obsolete') AND status_last_seen < :last_import";
  466. $oSetToDelete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToDelete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sDeletionDate));
  467. while($oReplica = $oSetToDelete->Fetch())
  468. {
  469. $oStatLog->Set('stats_nb_deleted', $oStatLog->Get('stats_nb_deleted') + 1);
  470. $sUpdateOnObsolete = $this->Get('delete_policy');
  471. if ( ($sUpdateOnObsolete == 'delete') || ($sUpdateOnObsolete == 'update_then_delete') )
  472. {
  473. // TO DO: remove trace
  474. echo "<p>Destination object: (dest_id:".$oReplica->Get('dest_id').") to be DELETED.</p>";
  475. // TO DO: delete the dest object for real...
  476. $oReplica->DeleteDestObject($oMyChange, $oStatLog);
  477. }
  478. // TO DO: remove trace
  479. echo "<p>Replica id:".$oReplica->GetKey()." (dest_id:".$oReplica->Get('dest_id').") to be deleted</p>";
  480. $oReplica->DBDeleteTracked($oMyChange);
  481. }
  482. }
  483. /**
  484. * Get the list of SQL columns corresponding to a particular list of attribute codes
  485. * Defaults to the whole list of columns for the current class
  486. */
  487. public function GetSQLColumns($aAttributeCodes = null)
  488. {
  489. $aColumns = array();
  490. $sClass = $this->GetTargetClass();
  491. if (is_null($aAttributeCodes))
  492. {
  493. $aAttributeCodes = array();
  494. foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
  495. {
  496. if ($sAttCode == 'finalclass') continue;
  497. $aAttributeCodes[] = $sAttCode;
  498. }
  499. }
  500. foreach($aAttributeCodes as $sAttCode)
  501. {
  502. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
  503. foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
  504. {
  505. $aColumns[$sField] = $sDBFieldType;
  506. }
  507. }
  508. return $aColumns;
  509. }
  510. public function IsRunning()
  511. {
  512. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='running'";
  513. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */);
  514. if ($oSet->Count() < 1)
  515. {
  516. $bRet = false;
  517. }
  518. else
  519. {
  520. $bRet = true;
  521. }
  522. return $bRet;
  523. }
  524. public function GetLatestLog()
  525. {
  526. $oLog = null;
  527. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id";
  528. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('start_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 1 /* limitCount */, 0 /* limitStart */);
  529. if ($oSet->Count() >= 1)
  530. {
  531. $oLog = $oSet->Fetch();
  532. }
  533. return $oLog;
  534. }
  535. // TO DO: remove if still unused
  536. /**
  537. * Retrieve from the log, the date of the last completed import
  538. * @return DateTime
  539. */
  540. public function GetLastCompletedImportDate()
  541. {
  542. $date = null;
  543. $sOQL = "SELECT SynchroLog WHERE sync_source_id = :source_id AND status='completed'";
  544. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array('end_date' => false) /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, array(), 0 /* limitCount */, 0 /* limitStart */);
  545. if ($oSet->Count() >= 1)
  546. {
  547. $oLog = $oSet->Fetch();
  548. $date = $oLog->Get('end_date');
  549. }
  550. else
  551. {
  552. // TO DO: remove trace
  553. echo "<p>No completed log found</p>\n";
  554. }
  555. return $date;
  556. }
  557. }
  558. class SynchroAttribute extends cmdbAbstractObject
  559. {
  560. public static function Init()
  561. {
  562. $aParams = array
  563. (
  564. "category" => "core/cmdb,view_in_gui",
  565. "key_type" => "autoincrement",
  566. "name_attcode" => "",
  567. "state_attcode" => "",
  568. "reconc_keys" => array(),
  569. "db_table" => "priv_sync_att",
  570. "db_key_field" => "id",
  571. "db_finalclass_field" => "",
  572. "display_template" => "",
  573. );
  574. MetaModel::Init_Params($aParams);
  575. MetaModel::Init_InheritAttributes();
  576. MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
  577. MetaModel::Init_AddAttribute(new AttributeString("attcode", array("allowed_values"=>null, "sql"=>"attcode", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  578. MetaModel::Init_AddAttribute(new AttributeBoolean("update", array("allowed_values"=>null, "sql"=>"update", "default_value"=>true, "is_null_allowed"=>false, "depends_on"=>array())));
  579. MetaModel::Init_AddAttribute(new AttributeBoolean("reconcile", array("allowed_values"=>null, "sql"=>"reconcile", "default_value"=>false, "is_null_allowed"=>false, "depends_on"=>array())));
  580. MetaModel::Init_AddAttribute(new AttributeEnum("update_policy", array("allowed_values"=>new ValueSetEnum('master_locked,master_unlocked,write_once'), "sql"=>"update_policy", "default_value"=>"master_locked", "is_null_allowed"=>false, "depends_on"=>array())));
  581. // Display lists
  582. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for the complete details
  583. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  584. // Search criteria
  585. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  586. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  587. }
  588. }
  589. class SynchroAttExtKey extends SynchroAttribute
  590. {
  591. public static function Init()
  592. {
  593. $aParams = array
  594. (
  595. "category" => "core/cmdb,view_in_gui",
  596. "key_type" => "autoincrement",
  597. "name_attcode" => "",
  598. "state_attcode" => "",
  599. "reconc_keys" => array(),
  600. "db_table" => "priv_sync_att_extkey",
  601. "db_key_field" => "id",
  602. "db_finalclass_field" => "",
  603. "display_template" => "",
  604. );
  605. MetaModel::Init_Params($aParams);
  606. MetaModel::Init_InheritAttributes();
  607. MetaModel::Init_AddAttribute(new AttributeString("reconciliation_attcode", array("allowed_values"=>null, "sql"=>"reconciliation_attcode", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  608. // Display lists
  609. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'reconciliation_attcode')); // Attributes to be displayed for the complete details
  610. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  611. // Search criteria
  612. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  613. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  614. }
  615. }
  616. class SynchroAttLinkSet extends SynchroAttribute
  617. {
  618. public static function Init()
  619. {
  620. $aParams = array
  621. (
  622. "category" => "core/cmdb,view_in_gui",
  623. "key_type" => "autoincrement",
  624. "name_attcode" => "",
  625. "state_attcode" => "",
  626. "reconc_keys" => array(),
  627. "db_table" => "priv_sync_att_linkset",
  628. "db_key_field" => "id",
  629. "db_finalclass_field" => "",
  630. "display_template" => "",
  631. );
  632. MetaModel::Init_Params($aParams);
  633. MetaModel::Init_InheritAttributes();
  634. MetaModel::Init_AddAttribute(new AttributeString("row_separator", array("allowed_values"=>null, "sql"=>"row_separator", "default_value"=>'|', "is_null_allowed"=>true, "depends_on"=>array())));
  635. MetaModel::Init_AddAttribute(new AttributeString("attribute_separator", array("allowed_values"=>null, "sql"=>"attribute_separator", "default_value"=>';', "is_null_allowed"=>true, "depends_on"=>array())));
  636. // Display lists
  637. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy', 'row_separator', 'attribute_separator')); // Attributes to be displayed for the complete details
  638. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'attcode', 'update', 'reconcile', 'update_policy')); // Attributes to be displayed for a list
  639. // Search criteria
  640. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  641. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  642. }
  643. }
  644. //class SynchroLog extends Event
  645. class SynchroLog extends cmdbAbstractObject
  646. {
  647. public static function Init()
  648. {
  649. $aParams = array
  650. (
  651. "category" => "core/cmdb,view_in_gui",
  652. "key_type" => "autoincrement",
  653. "name_attcode" => "",
  654. "state_attcode" => "",
  655. "reconc_keys" => array(),
  656. "db_table" => "priv_sync_log",
  657. "db_key_field" => "id",
  658. "db_finalclass_field" => "",
  659. "display_template" => "",
  660. );
  661. MetaModel::Init_Params($aParams);
  662. MetaModel::Init_InheritAttributes();
  663. // MetaModel::Init_AddAttribute(new AttributeString("userinfo", array("allowed_values"=>null, "sql"=>"userinfo", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  664. MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
  665. MetaModel::Init_AddAttribute(new AttributeDateTime("start_date", array("allowed_values"=>null, "sql"=>"start_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  666. MetaModel::Init_AddAttribute(new AttributeDateTime("end_date", array("allowed_values"=>null, "sql"=>"end_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  667. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('running,completed'), "sql"=>"status", "default_value"=>"running", "is_null_allowed"=>false, "depends_on"=>array())));
  668. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_seen", array("allowed_values"=>null, "sql"=>"stats_nb_seen", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  669. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_modified", array("allowed_values"=>null, "sql"=>"stats_nb_modified", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  670. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_errors", array("allowed_values"=>null, "sql"=>"stats_nb_errors", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  671. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_created", array("allowed_values"=>null, "sql"=>"stats_nb_created", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  672. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_deleted", array("allowed_values"=>null, "sql"=>"stats_nb_deleted", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  673. MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_reconciled", array("allowed_values"=>null, "sql"=>"stats_nb_reconciled", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array())));
  674. // Display lists
  675. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_seen', 'stats_nb_modified', 'stats_nb_errors', 'stats_nb_created', 'stats_nb_deleted', 'stats_nb_reconciled')); // Attributes to be displayed for the complete details
  676. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_seen', 'stats_nb_modified', 'stats_nb_errors')); // Attributes to be displayed for a list
  677. // Search criteria
  678. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  679. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  680. }
  681. }
  682. class SynchroReplica extends DBObject
  683. {
  684. static $aSearches = array(); // Cache of OQL queries used for reconciliation (per data source)
  685. public static function Init()
  686. {
  687. $aParams = array
  688. (
  689. "category" => "core/cmdb,view_in_gui",
  690. "key_type" => "autoincrement",
  691. "name_attcode" => "",
  692. "state_attcode" => "",
  693. "reconc_keys" => array(),
  694. "db_table" => "priv_sync_replica",
  695. "db_key_field" => "id",
  696. "db_finalclass_field" => "",
  697. "display_template" => "",
  698. );
  699. MetaModel::Init_Params($aParams);
  700. MetaModel::Init_InheritAttributes();
  701. MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
  702. MetaModel::Init_AddAttribute(new AttributeInteger("dest_id", array("allowed_values"=>null, "sql"=>"dest_id", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  703. MetaModel::Init_AddAttribute(new AttributeClass("dest_class", array("class_category"=>"bizmodel", "more_values"=>"", "sql"=>"dest_class", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  704. MetaModel::Init_AddAttribute(new AttributeDateTime("status_last_seen", array("allowed_values"=>null, "sql"=>"status_last_seen", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
  705. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('new,synchronized,modified,orphan,obsolete'), "sql"=>"status", "default_value"=>"new", "is_null_allowed"=>false, "depends_on"=>array())));
  706. MetaModel::Init_AddAttribute(new AttributeBoolean("status_dest_creator", array("allowed_values"=>null, "sql"=>"status_dest_creator", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  707. MetaModel::Init_AddAttribute(new AttributeString("status_last_error", array("allowed_values"=>null, "sql"=>"status_last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
  708. MetaModel::Init_AddAttribute(new AttributeDateTime("info_creation_date", array("allowed_values"=>null, "sql"=>"info_creation_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  709. MetaModel::Init_AddAttribute(new AttributeDateTime("info_last_modified", array("allowed_values"=>null, "sql"=>"info_last_modified", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  710. MetaModel::Init_AddAttribute(new AttributeDateTime("info_last_synchro", array("allowed_values"=>null, "sql"=>"info_last_synchro", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  711. // Display lists
  712. MetaModel::Init_SetZListItems('details', array('sync_source_id', 'dest_id', 'dest_class', 'status_last_seen', 'status', 'status_dest_creator', 'status_last_error', 'info_creation_date', 'info_last_modified', 'info_last_synchro')); // Attributes to be displayed for the complete details
  713. MetaModel::Init_SetZListItems('list', array('sync_source_id', 'dest_id', 'dest_class', 'status_last_seen', 'status', 'status_dest_creator', 'status_last_error')); // Attributes to be displayed for a list
  714. // Search criteria
  715. MetaModel::Init_SetZListItems('standard_search', array('sync_source_id', 'status_last_seen', 'status', 'status_dest_creator', 'dest_class', 'dest_id', 'status_last_error')); // Criteria of the std search form
  716. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  717. }
  718. public function DBInsert()
  719. {
  720. throw new CoreException('A synchronization replica must be created only by the mean of triggers');
  721. }
  722. // Overload the deletion -> the replica has been created by the mean of a trigger,
  723. // it will be deleted by the mean of a trigger too
  724. public function DBDelete()
  725. {
  726. $oDataSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'));
  727. $sTable = $oDataSource->GetDataTable();
  728. $sSQL = "DELETE FROM `$sTable` WHERE id = '{$this->GetKey()}'";
  729. CMDBSource::Query($sSQL);
  730. $this->m_bIsInDB = false;
  731. $this->m_iKey = null;
  732. }
  733. public function SetLastError($sMessage, $oException = null)
  734. {
  735. if ($oException)
  736. {
  737. $sText = $sMessage.$oException->getMessage();
  738. }
  739. else
  740. {
  741. $sText = $sMessage;
  742. }
  743. if (strlen($sText) > 255)
  744. {
  745. $sText = substr($sText, 0, 200).'...('.strlen($sText).' chars)...';
  746. }
  747. $this->Set('status_last_error', $sText);
  748. }
  749. public function Synchro($oDataSource, $aReconciliationKeys, $aAttributes, $oChange, $oStatLog)
  750. {
  751. switch($this->Get('status'))
  752. {
  753. case 'new':
  754. // If needed, construct the query used for the reconciliation
  755. if (!isset(self::$aSearches[$oDataSource->GetKey()]))
  756. {
  757. foreach($aReconciliationKeys as $sFilterCode)
  758. {
  759. $aCriterias[] = ($sFilterCode == 'primary_key' ? 'id' : $sFilterCode).' = :'.$sFilterCode;
  760. }
  761. $sOQL = "SELECT ".$oDataSource->GetTargetClass()." WHERE ".implode(' AND ', $aCriterias);
  762. self::$aSearches[$oDataSource->GetKey()] = DBObjectSearch::FromOQL($sOQL);
  763. }
  764. // Get the criterias for the search
  765. $aFilterValues = array();
  766. foreach($aReconciliationKeys as $sFilterCode)
  767. {
  768. $aFilterValues[$sFilterCode] = $this->GetValueFromExtData($sFilterCode);
  769. }
  770. $oDestSet = new DBObjectSet(self::$aSearches[$oDataSource->GetKey()], array(), $aFilterValues);
  771. $iCount = $oDestSet->Count();
  772. // How many objects match the reconciliation criterias
  773. switch($iCount)
  774. {
  775. case 0:
  776. //echo "<p>Nothing found for: ".self::$aSearches[$oDataSource->GetKey()]->ToOQL(true, $aFilterValues)."</p>";
  777. $this->CreateObjectFromReplica($oDataSource->GetTargetClass(), $aAttributes, $oChange, $oStatLog);
  778. break;
  779. case 1:
  780. //echo "<p>Found 1 for: ".self::$aSearches[$oDataSource->GetKey()]->ToOQL(true, $aFilterValues)."</p>";
  781. $oDestObj = $oDestSet->Fetch();
  782. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog);
  783. $this->Set('dest_id', $oDestObj->GetKey());
  784. $this->Set('status_dest_creator', false);
  785. $this->Set('dest_class', get_class($oDestObj));
  786. $oStatLog->Set('stats_nb_reconciled', $oStatLog->Get('stats_nb_reconciled') + 1);
  787. break;
  788. default:
  789. $aConditions = array();
  790. foreach($aFilterValues as $sCode => $sValue)
  791. {
  792. $aConditions[] = $sCode.'='.$sValue;
  793. }
  794. $sCondition = implode(' AND ', $aConditions);
  795. //echo "<p>Found N for: ".self::$aSearches[$oDataSource->GetKey()]->ToOQL(true, $aFilterValues)."</p>";
  796. $this->SetLastError($iCount.' destination objects match the reconciliation criterias: '.$sCondition);
  797. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  798. }
  799. break;
  800. case 'modified':
  801. $oDestObj = MetaModel::GetObject($oDataSource->GetTargetClass(), $this->Get('dest_id'));
  802. if ($oDestObj == null)
  803. {
  804. $this->Set('status', 'orphan'); // The destination object has been deleted !
  805. $this->SetLastError('Destination object deleted unexpectedly');
  806. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  807. }
  808. else
  809. {
  810. $this->UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, $oStatLog);
  811. }
  812. break;
  813. default: // Do nothing in all other cases
  814. }
  815. $this->DBUpdateTracked($oChange);
  816. }
  817. /**
  818. * Updates the destination object with the Extended data found in the synchro_data_XXXX table
  819. */
  820. protected function UpdateObjectFromReplica($oDestObj, $aAttributes, $oChange, &$oStatLog)
  821. {
  822. // TO DO: remove trace
  823. echo "<p>Update object ".$oDestObj->GetHyperLink()."</p>";
  824. foreach($aAttributes as $sAttCode)
  825. {
  826. $value = $this->GetValueFromExtData($sAttCode);
  827. $oDestObj->Set($sAttCode, $value);
  828. // TO DO: remove trace
  829. echo "<p>&nbsp;&nbsp;&nbsp;Setting $sAttCode to $value</p>";
  830. }
  831. try
  832. {
  833. $oDestObj->DBUpdateTracked($oChange);
  834. $this->Set('status_last_error', '');
  835. $this->Set('status', 'synchronized');
  836. }
  837. catch(Exception $e)
  838. {
  839. $this->SetLastError('Unable to update destination object: ', $e);
  840. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  841. }
  842. }
  843. /**
  844. * Creates the destination object populating it with the Extended data found in the synchro_data_XXXX table
  845. */
  846. protected function CreateObjectFromReplica($sClass, $aAttributes, $oChange, $oStatLog)
  847. {
  848. // TO DO: remove trace
  849. echo "<p>Creating new $sClass</p>";
  850. $oDestObj = MetaModel::NewObject($sClass);
  851. foreach($aAttributes as $sAttCode)
  852. {
  853. $value = $this->GetValueFromExtData($sAttCode);
  854. $oDestObj->Set($sAttCode, $value);
  855. // TO DO: remove trace
  856. echo "<p>&nbsp;&nbsp;&nbsp;Setting $sAttCode to $value</p>";
  857. }
  858. try
  859. {
  860. $iNew = $oDestObj->DBInsertTracked($oChange);
  861. // TO DO: remove trace
  862. echo "<p>Created: $iNew</p>";
  863. $this->Set('dest_id', $oDestObj->GetKey());
  864. $this->Set('dest_class', get_class($oDestObj));
  865. $this->Set('status_dest_creator', true);
  866. $this->Set('status_last_error', '');
  867. $this->Set('status', 'synchronized');
  868. $oStatLog->Set('stats_nb_created', $oStatLog->Get('stats_nb_created') + 1);
  869. }
  870. catch(Exception $e)
  871. {
  872. $this->SetLastError('Unable to create destination object: ', $e);
  873. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  874. }
  875. }
  876. /**
  877. * Update the destination object with given values
  878. */
  879. public function UpdateDestObject($aValues, $oChange, &$oStatLog)
  880. {
  881. try
  882. {
  883. $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'));
  884. foreach($aValues as $sAttCode => $value)
  885. {
  886. $oDestObj->Set($sAttCode, $value);
  887. }
  888. $oDestObj->DBUpdateTracked($oChange);
  889. }
  890. catch(Exception $e)
  891. {
  892. $this->SetLastError('Unable to update the destination object: ', $e);
  893. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  894. }
  895. }
  896. /**
  897. * Delete the destination object
  898. */
  899. public function DeleteDestObject($oChange, &$oStatLog)
  900. {
  901. if($this->Get('status_dest_creator'))
  902. {
  903. $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'));
  904. try
  905. {
  906. $oDestObj->DBDeleteTracked($oChange);
  907. }
  908. catch(Exception $e)
  909. {
  910. $this->SetLastError('Unable to delete the destination object: ', $e);
  911. $oStatLog->Set('stats_nb_errors', $oStatLog->Get('stats_nb_errors') + 1);
  912. }
  913. }
  914. }
  915. /**
  916. * Get the value from the 'Extended Data' located in the synchro_data_xxx table for this replica
  917. */
  918. protected function GetValueFromExtData($sColumnName)
  919. {
  920. $aData = $this->GetExtendedData();
  921. return $aData[$sColumnName];
  922. }
  923. }
  924. // TO DO: finalize.... admins only ? which options ? troubleshoot WebPageMenuNode::__construct(.... sEnableClass...) ?
  925. //if (UserRights::IsAdministrator())
  926. {
  927. $oAdminMenu = new MenuGroup('AdminTools', 80 /* fRank */);
  928. new OQLMenuNode('DataSources', 'SELECT SynchroDataSource', $oAdminMenu->GetIndex(), 12 /* fRank */, true, 'SynchroDataSource', UR_ACTION_MODIFY, UR_ALLOWED_YES);
  929. new WebPageMenuNode('Test:RunSynchro', '../synchro/synchro_exec.php', $oAdminMenu->GetIndex(), 13 /* fRank */, 'SynchroDataSource');
  930. }
  931. ?>