relationgraph.class.inc.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. <?php
  2. // Copyright (C) 2015 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Data structures (i.e. PHP classes) to build and use relation graphs
  20. *
  21. * @copyright Copyright (C) 2015 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. *
  24. */
  25. require_once(APPROOT.'core/simplegraph.class.inc.php');
  26. /**
  27. * An object Node inside a RelationGraph
  28. */
  29. class RelationObjectNode extends GraphNode
  30. {
  31. public function __construct($oGraph, $oObject)
  32. {
  33. parent::__construct($oGraph, self::MakeId($oObject));
  34. $this->SetProperty('object', $oObject);
  35. $this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')');
  36. }
  37. /**
  38. * Make a normalized ID to ensure the uniqueness of such a node
  39. */
  40. public static function MakeId($oObject)
  41. {
  42. return get_class($oObject).'::'.$oObject->GetKey();
  43. }
  44. /**
  45. * Formatting for GraphViz
  46. */
  47. public function GetDotAttributes()
  48. {
  49. $sDot = parent::GetDotAttributes();
  50. if ($this->GetProperty('developped', false))
  51. {
  52. $sDot .= ',fontcolor=black';
  53. }
  54. else
  55. {
  56. $sDot .= ',fontcolor=lightgrey';
  57. }
  58. if ($this->GetProperty('source', false) || $this->GetProperty('sink', false))
  59. {
  60. $sDot .= ',shape=rectangle';
  61. }
  62. if ($this->GetProperty('is_reached', false))
  63. {
  64. $sDot .= ',fillcolor="#ffdddd"';
  65. }
  66. else
  67. {
  68. $sDot .= ',fillcolor=white';
  69. }
  70. return $sDot;
  71. }
  72. /**
  73. * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
  74. */
  75. public function ReachDown()
  76. {
  77. if (!$this->GetProperty('is_reached', false))
  78. {
  79. $this->SetProperty('is_reached', true);
  80. foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
  81. {
  82. // Recurse
  83. $oOutgoingEdge->GetSinkNode()->ReachDown();
  84. }
  85. }
  86. }
  87. }
  88. /**
  89. * An redundancy Node inside a RelationGraph
  90. */
  91. class RelationRedundancyNode extends GraphNode
  92. {
  93. public function __construct($oGraph, $sId, $iMinUp, $fThreshold)
  94. {
  95. parent::__construct($oGraph, $sId);
  96. $this->SetProperty('min_up', $iMinUp);
  97. $this->SetProperty('threshold', $fThreshold);
  98. $this->SetProperty('reach_count', 0);
  99. }
  100. /**
  101. * Make a normalized ID to ensure the uniqueness of such a node
  102. */
  103. public static function MakeId($sRelCode, $sNeighbourId, $oSinkObject)
  104. {
  105. return 'redundancy-'.$sRelCode.'-'.$sNeighbourId.'-'.get_class($oSinkObject).'::'.$oSinkObject->GetKey();
  106. }
  107. /**
  108. * Formatting for GraphViz
  109. */
  110. public function GetDotAttributes()
  111. {
  112. $sDisplayThreshold = sprintf('%.1f', $this->GetProperty('threshold'));
  113. $sDot = 'shape=doublecircle,fillcolor=indianred,fontcolor=papayawhip,label="'.$sDisplayThreshold.'"';
  114. return $sDot;
  115. }
  116. /**
  117. * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
  118. */
  119. public function ReachDown()
  120. {
  121. $this->SetProperty('reach_count', $this->GetProperty('reach_count', 0) + 1);
  122. if ($this->GetProperty('reach_count') > $this->GetProperty('threshold'))
  123. {
  124. // Looping... though there should be only ONE SINGLE outgoing edge
  125. foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
  126. {
  127. // Recurse
  128. $oOutgoingEdge->GetSinkNode()->ReachDown();
  129. }
  130. }
  131. }
  132. }
  133. /**
  134. * Helper to name the edges in a unique way
  135. */
  136. class RelationEdge extends GraphEdge
  137. {
  138. public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode)
  139. {
  140. $sId = $oSourceNode->GetId().'-to-'.$oSinkNode->GetId();
  141. parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode);
  142. }
  143. }
  144. /**
  145. * A graph representing the relations between objects
  146. * The graph is made of two types of nodes. Here is a list of the meaningful node properties
  147. * 1) RelationObjectNode
  148. * source: boolean, that node was added as a source node
  149. * sink: boolean, that node was added as a sink node
  150. * reached: boolean, that node has been marked as reached (impacted by the source nodes)
  151. * developped: boolean, that node has been visited to search for related objects
  152. * 1) RelationRedundancyNode
  153. * reached_count: int, the number of source nodes having reached=true
  154. * threshold: float, if reached_count > threshold, the sink nodes become reachable
  155. */
  156. class RelationGraph extends SimpleGraph
  157. {
  158. protected $aSourceNodes; // Index of source nodes (for a quicker access)
  159. protected $aSinkNodes; // Index of sink nodes (for a quicker access)
  160. protected $aRedundancySettings; // Cache of user settings
  161. public function __construct()
  162. {
  163. parent::__construct();
  164. $this->aSourceNodes = array();
  165. $this->aSinkNodes = array();
  166. $this->aRedundancySettings = array();
  167. }
  168. /**
  169. * Add an object that will be the starting point for building the relations downstream
  170. */
  171. public function AddSourceObject(DBObject $oObject)
  172. {
  173. $oSourceNode = new RelationObjectNode($this, $oObject);
  174. $oSourceNode->SetProperty('source', true);
  175. $this->aSourceNodes[$oSourceNode->GetId()] = $oSourceNode;
  176. }
  177. /**
  178. * Add an object that will be the starting point for building the relations uptream
  179. */
  180. public function AddSinkObject(DBObject$oObject)
  181. {
  182. $oSinkNode = new RelationObjectNode($this, $oObject);
  183. $oSinkNode->SetProperty('sink', true);
  184. $this->aSinkNodes[$oSinkNode->GetId()] = $oSinkNode;
  185. }
  186. /**
  187. * Build the graph downstream, and mark the nodes that can be reached from the source node
  188. */
  189. public function ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy)
  190. {
  191. //echo "<h5>Sources only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
  192. // Build the graph out of the sources
  193. foreach ($this->aSourceNodes as $oSourceNode)
  194. {
  195. $this->AddRelatedObjects($sRelCode, true, $oSourceNode, $iMaxDepth, $bEnableRedundancy);
  196. //echo "<h5>After processing of {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
  197. }
  198. // Determine the reached nodes
  199. foreach ($this->aSourceNodes as $oSourceNode)
  200. {
  201. $oSourceNode->ReachDown();
  202. //echo "<h5>After reaching from {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
  203. }
  204. }
  205. /**
  206. * Build the graph upstream
  207. */
  208. public function ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy)
  209. {
  210. //echo "<h5>Sinks only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
  211. // Build the graph out of the sinks
  212. foreach ($this->aSinkNodes as $oSinkNode)
  213. {
  214. $this->AddRelatedObjects($sRelCode, false, $oSinkNode, $iMaxDepth, $bEnableRedundancy);
  215. //echo "<h5>After processing of {$oSinkNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
  216. }
  217. }
  218. /**
  219. * Recursively find related objects, and add them into the graph
  220. *
  221. * @param string $sRelCode The code of the relation to use for the computation
  222. * @param boolean $bDown The direction: downstream or upstream
  223. * @param array $oObjectNode The node from which to compute the neighbours
  224. * @param int $iMaxDepth
  225. * @param boolean $bEnableReduncancy
  226. *
  227. * @return void
  228. */
  229. protected function AddRelatedObjects($sRelCode, $bDown, $oObjectNode, $iMaxDepth, $bEnableRedundancy)
  230. {
  231. if ($iMaxDepth > 0)
  232. {
  233. if ($oObjectNode instanceof RelationRedundancyNode)
  234. {
  235. // Note: this happens when recursing on an existing part of the graph
  236. // Skip that redundancy node
  237. $aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges();
  238. foreach ($aRelatedEdges as $oRelatedEdge)
  239. {
  240. $oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode();
  241. // Recurse (same depth)
  242. $this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth, $bEnableRedundancy);
  243. }
  244. }
  245. elseif ($oObjectNode->GetProperty('developped', false))
  246. {
  247. // No need to execute the queries again... just dig into the nodes down/up to iMaxDepth
  248. //
  249. $aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges();
  250. foreach ($aRelatedEdges as $oRelatedEdge)
  251. {
  252. $oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode();
  253. // Recurse (decrement the depth)
  254. $this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
  255. }
  256. }
  257. else
  258. {
  259. $oObjectNode->SetProperty('developped', true);
  260. $oObject = $oObjectNode->GetProperty('object');
  261. foreach (MetaModel::EnumRelationQueries(get_class($oObject), $sRelCode, $bDown) as $sDummy => $aQueryInfo)
  262. {
  263. $sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp'];
  264. try
  265. {
  266. $oFlt = DBObjectSearch::FromOQL($sQuery);
  267. $oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery());
  268. $oRelatedObj = $oObjSet->Fetch();
  269. }
  270. catch (Exception $e)
  271. {
  272. $sDirection = $bDown ? 'downstream' : 'upstream';
  273. throw new Exception("Wrong query ($sDirection) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
  274. }
  275. if ($oRelatedObj)
  276. {
  277. do
  278. {
  279. $sObjectRef = RelationObjectNode::MakeId($oRelatedObj);
  280. $oRelatedNode = $this->GetNode($sObjectRef);
  281. if (is_null($oRelatedNode))
  282. {
  283. $oRelatedNode = new RelationObjectNode($this, $oRelatedObj);
  284. }
  285. $oSourceNode = $bDown ? $oObjectNode : $oRelatedNode;
  286. $oSinkNode = $bDown ? $oRelatedNode : $oObjectNode;
  287. if ($bEnableRedundancy)
  288. {
  289. $oRedundancyNode = $this->ComputeRedundancy($sRelCode, $aQueryInfo, $oSourceNode, $oSinkNode);
  290. }
  291. else
  292. {
  293. $oRedundancyNode = null;
  294. }
  295. if (!$oRedundancyNode)
  296. {
  297. // Direct link (otherwise handled by ComputeRedundancy)
  298. $oEdge = new RelationEdge($this, $oSourceNode, $oSinkNode);
  299. }
  300. // Recurse
  301. $this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
  302. }
  303. while ($oRelatedObj = $oObjSet->Fetch());
  304. }
  305. }
  306. }
  307. }
  308. }
  309. /**
  310. * Determine if there is a redundancy (or use the existing one) and add the corresponding nodes/edges
  311. */
  312. protected function ComputeRedundancy($sRelCode, $aQueryInfo, $oFromNode, $oToNode)
  313. {
  314. $oRedundancyNode = null;
  315. $oObject = $oToNode->GetProperty('object');
  316. if ($this->IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode))
  317. {
  318. $sId = RelationRedundancyNode::MakeId($sRelCode, $aQueryInfo['sNeighbour'], $oToNode->GetProperty('object'));
  319. $oRedundancyNode = $this->GetNode($sId);
  320. if (is_null($oRedundancyNode))
  321. {
  322. // Get the upper neighbours
  323. $sQuery = $aQueryInfo['sQueryUp'];
  324. try
  325. {
  326. $oFlt = DBObjectSearch::FromOQL($sQuery);
  327. $oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery());
  328. $iCount = $oObjSet->Count();
  329. }
  330. catch (Exception $e)
  331. {
  332. throw new Exception("Wrong query (upstream) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
  333. }
  334. $iMinUp = $this->GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iCount);
  335. $fThreshold = max(0, $iCount - $iMinUp);
  336. $oRedundancyNode = new RelationRedundancyNode($this, $sId, $iMinUp, $fThreshold);
  337. new RelationEdge($this, $oRedundancyNode, $oToNode);
  338. while ($oUpperObj = $oObjSet->Fetch())
  339. {
  340. $sObjectRef = RelationObjectNode::MakeId($oUpperObj);
  341. $oUpperNode = $this->GetNode($sObjectRef);
  342. if (is_null($oUpperNode))
  343. {
  344. $oUpperNode = new RelationObjectNode($this, $oUpperObj);
  345. }
  346. new RelationEdge($this, $oUpperNode, $oRedundancyNode);
  347. }
  348. }
  349. }
  350. return $oRedundancyNode;
  351. }
  352. /**
  353. * Helper to determine the redundancy setting on a given relation
  354. */
  355. protected function IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode)
  356. {
  357. $bRet = false;
  358. if (isset($aQueryInfo['sRedundancyEnabledMode']))
  359. {
  360. if ($aQueryInfo['sRedundancyEnabledMode'] == 'fixed')
  361. {
  362. $bRet = $aQueryInfo['bRedundancyEnabledValue'];
  363. }
  364. elseif ($aQueryInfo['sRedundancyEnabledMode'] == 'user')
  365. {
  366. $oUserSettings = $this->FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode);
  367. $bRet = $oUserSettings->Get('enabled');
  368. }
  369. }
  370. return $bRet;
  371. }
  372. /**
  373. * Helper to determine the redundancy threshold, given the count of objects upstream
  374. */
  375. protected function GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iUpstreamObjects)
  376. {
  377. $iMinUp = 0;
  378. if (isset($aQueryInfo['sRedundancyMinUpMode']))
  379. {
  380. if ($aQueryInfo['sRedundancyMinUpMode'] == 'fixed')
  381. {
  382. if ($aQueryInfo['sRedundancyMinUpType'] == 'count')
  383. {
  384. $iMinUp = $aQueryInfo['iRedundancyMinUpValue'];
  385. }
  386. else // 'percent' assumed
  387. {
  388. $iMinUp = $iUpstreamObjects * $aQueryInfo['iRedundancyMinUpValue'] / 100;
  389. }
  390. }
  391. elseif ($aQueryInfo['sRedundancyMinUpMode'] == 'user')
  392. {
  393. $oUserSettings = $this->FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode);
  394. if ($oUserSettings->Get('min_up_type') == 'count')
  395. {
  396. $iMinUp = $oUserSettings->Get('min_up_count');
  397. }
  398. else
  399. {
  400. $iMinUp = $iUpstreamObjects * $oUserSettings->Get('min_up_percent') / 100;
  401. }
  402. }
  403. }
  404. return $iMinUp;
  405. }
  406. /**
  407. * Helper to search for and cache the reduncancy user settings (could be an object NOT recorded in the DB)
  408. */
  409. protected function FindRedundancyUserSettings($sRelCode, $aQueryInfo, $oToNode)
  410. {
  411. $sNeighbourKey = $sRelCode.'/'.$aQueryInfo['sFromClass'].'/'.$aQueryInfo['sNeighbour'];
  412. if (isset($this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()]))
  413. {
  414. // Cache hit
  415. $oUserSettings = $this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()];
  416. }
  417. else
  418. {
  419. // Cache miss: build the entry
  420. $oUserSettings = RedundancySettings::GetSettings($sRelCode, $aQueryInfo, $oToNode->GetProperty('object'));
  421. $this->aRedundancySettings[$sNeighbourKey][$oToNode->GetId()] = $oUserSettings;
  422. }
  423. return $oUserSettings;
  424. }
  425. }