1<?php
2// Copyright (C) 2015-2018 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
20/**
21 * Data structures (i.e. PHP classes) to build and use relation graphs
22 *
23 * @copyright   Copyright (C) 2015-2018 Combodo SARL
24 * @license     http://opensource.org/licenses/AGPL-3.0
25 *
26 */
27
28require_once(APPROOT.'core/simplegraph.class.inc.php');
29
30/**
31 * An object Node inside a RelationGraph
32 */
33class RelationObjectNode extends GraphNode
34{
35	public function __construct($oGraph, $oObject)
36	{
37		parent::__construct($oGraph, self::MakeId($oObject));
38		$this->SetProperty('object', $oObject);
39		$this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')');
40	}
41
42	/**
43	 * Make a normalized ID to ensure the uniqueness of such a node
44	 *
45	 * @param string $oObject
46	 *
47	 * @return string
48	 */
49	public static function MakeId($oObject)
50	{
51		return get_class($oObject).'::'.$oObject->GetKey();
52	}
53
54	/**
55	 * Formatting for GraphViz
56	 *
57	 * @param bool $bNoLabel
58	 *
59	 * @return string
60	 */
61	public function GetDotAttributes($bNoLabel = false)
62	{
63		$sDot = parent::GetDotAttributes();
64		if ($this->GetProperty('developped', false))
65		{
66			$sDot .= ',fontcolor=black';
67		}
68		else
69		{
70			$sDot .= ',fontcolor=lightgrey';
71		}
72		if ($this->GetProperty('source', false) || $this->GetProperty('sink', false))
73		{
74			$sDot .= ',shape=rectangle';
75		}
76		if ($this->GetProperty('is_reached', false))
77		{
78			$sDot .= ',fillcolor="#ffdddd"';
79		}
80		else
81		{
82			$sDot .= ',fillcolor=white';
83		}
84		return $sDot;
85	}
86
87	/**
88	 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node or a 'not allowed' node
89	 *
90	 * @param string $sProperty
91	 * @param $value
92	 */
93	public function ReachDown($sProperty, $value)
94	{
95		if (is_null($this->GetProperty($sProperty)) && ($this->GetProperty($sProperty.'_allowed') !== false))
96		{
97			$this->SetProperty($sProperty, $value);
98			foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
99			{
100				// Recurse
101				$oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value);
102			}
103		}
104	}
105}
106
107/**
108 * An redundancy Node inside a RelationGraph
109 */
110class RelationRedundancyNode extends GraphNode
111{
112	public function __construct($oGraph, $sId, $iMinUp, $fThreshold)
113	{
114		parent::__construct($oGraph, $sId);
115		$this->SetProperty('min_up', $iMinUp);
116		$this->SetProperty('threshold', $fThreshold);
117	}
118
119	/**
120	 * Make a normalized ID to ensure the uniqueness of such a node
121	 *
122	 * @param string $sRelCode
123	 * @param string $sNeighbourId
124	 * @param $oSourceObject
125	 * @param \DBObject $oSinkObject
126	 *
127	 * @return string
128	 */
129	public static function MakeId($sRelCode, $sNeighbourId, $oSourceObject, $oSinkObject)
130	{
131		return 'redundancy-'.$sRelCode.'-'.$sNeighbourId.'-'.get_class($oSinkObject).'::'.$oSinkObject->GetKey();
132	}
133
134	/**
135	 * Formatting for GraphViz
136	 *
137	 * @param bool $bNoLabel
138	 *
139	 * @return string
140	 */
141	public function GetDotAttributes($bNoLabel = false)
142	{
143		$sDisplayThreshold = sprintf('%.1f', $this->GetProperty('threshold'));
144		$sDot = 'shape=doublecircle,fillcolor=indianred,fontcolor=papayawhip,label="'.$sDisplayThreshold.'"';
145		return $sDot;
146	}
147
148	/**
149	 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node
150	 *
151	 * @param string $sProperty
152	 * @param $value
153	 */
154	public function ReachDown($sProperty, $value)
155	{
156		$this->SetProperty($sProperty.'_count', $this->GetProperty($sProperty.'_count', 0) + 1);
157		if ($this->GetProperty($sProperty.'_count') > $this->GetProperty('threshold'))
158		{
159			// Looping... though there should be only ONE SINGLE outgoing edge
160			foreach ($this->GetOutgoingEdges() as $oOutgoingEdge)
161			{
162				// Recurse
163				$oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value);
164			}
165		}
166	}
167}
168
169
170/**
171 * Helper to name the edges in a unique way
172 */
173class RelationEdge extends GraphEdge
174{
175	/**
176	 * RelationEdge constructor.
177	 *
178	 * @param \SimpleGraph $oGraph
179	 * @param \GraphNode $oSourceNode
180	 * @param \GraphNode $oSinkNode
181	 * @param bool $bMustBeUnique
182	 *
183	 * @throws \SimpleGraphException
184	 */
185	public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode, $bMustBeUnique = false)
186	{
187		$sId = $oSourceNode->GetId().'-to-'.$oSinkNode->GetId();
188		parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode, $bMustBeUnique);
189	}
190}
191
192/**
193 * A graph representing the relations between objects
194 * The graph is made of two types of nodes. Here is a list of the meaningful node properties
195 * 1) RelationObjectNode
196 *    source: boolean, that node was added as a source node
197 *    sink: boolean, that node was added as a sink node
198 *    reached: boolean, that node has been marked as reached (impacted by the source nodes)
199 *    developped: boolean, that node has been visited to search for related objects
200 * 1) RelationRedundancyNode
201 *    reached_count: int, the number of source nodes having reached=true
202 *    threshold: float, if reached_count > threshold, the sink nodes become reachable
203 */
204class RelationGraph extends SimpleGraph
205{
206	protected $aSourceNodes; // Index of source nodes (for a quicker access)
207	protected $aSinkNodes; // Index of sink nodes (for a quicker access)
208	protected $aRedundancySettings; // Cache of user settings
209	protected $aContextSearches; // Context ("knowing that") stored as a hash array 'class' => DBObjectSearch
210
211	public function __construct()
212	{
213		parent::__construct();
214		$this->aSourceNodes = array();
215		$this->aSinkNodes = array();
216		$this->aRedundancySettings = array();
217		$this->aContextSearches = array();
218	}
219
220	/**
221	 * Add an object that will be the starting point for building the relations downstream
222	 *
223	 * @param \DBObject $oObject
224	 */
225	public function AddSourceObject(DBObject $oObject)
226	{
227		$oSourceNode = new RelationObjectNode($this, $oObject);
228		$oSourceNode->SetProperty('source', true);
229		$this->aSourceNodes[$oSourceNode->GetId()] = $oSourceNode;
230	}
231
232	/**
233	 * Add an object that will be the starting point for building the relations uptream
234	 *
235	 * @param \DBObject $oObject
236	 */
237	public function AddSinkObject(DBObject$oObject)
238	{
239		$oSinkNode = new RelationObjectNode($this, $oObject);
240		$oSinkNode->SetProperty('sink', true);
241		$this->aSinkNodes[$oSinkNode->GetId()] = $oSinkNode;
242	}
243
244	/**
245	 * Add a 'context' OQL query, specifying extra objects to be marked as 'is_reached'
246	 * even though they are not part of the sources.
247	 *
248	 * @param string $key
249	 * @param string $sOQL The OQL query defining the context objects
250	 *
251	 * @throws \Exception
252	 */
253	public function AddContextQuery($key, $sOQL)
254	{
255		if ($sOQL === '') { return;}
256
257		$oSearch = static::MakeSearch($sOQL);
258		$aAliases = $oSearch->GetSelectedClasses();
259		if (count($aAliases) < 2 )
260		{
261			IssueLog::Error("Invalid context query '$sOQL'. A context query must contain at least two columns.");
262			throw new Exception("Invalid context query '$sOQL'. A context query must contain at least two columns. Columns: ".implode(', ', $aAliases).'. ');
263		}
264		$aAliasNames = array_keys($aAliases);
265		$oCondition = new BinaryExpression(new FieldExpression('id', $aAliasNames[0]), '=', new VariableExpression('id'));
266		$oSearch->AddConditionExpression($oCondition);
267
268		$sClass = $oSearch->GetClass();
269		if (!array_key_exists($sClass, $this->aContextSearches))
270		{
271			$this->aContextSearches[$sClass] = array();
272		}
273		$this->aContextSearches[$sClass][] = array('key' => $key, 'search' => $oSearch);
274	}
275
276	/**
277	 * Determines if the given DBObject is part of a 'context'
278	 *
279	 * @param DBObject $oObj
280	 *
281	 * @return boolean
282	 * @throws \CoreException
283	 */
284	public function IsPartOfContext(DBObject $oObj, &$aRootCauses)
285	{
286		$bRet = false;
287		$sFinalClass = get_class($oObj);
288		$aParentClasses = MetaModel::EnumParentClasses($sFinalClass, ENUM_PARENT_CLASSES_ALL);
289
290		foreach($aParentClasses as $sClass)
291		{
292			if (array_key_exists($sClass, $this->aContextSearches))
293			{
294				foreach($this->aContextSearches[$sClass] as $aContextQuery)
295				{
296					$aAliases = $aContextQuery['search']->GetSelectedClasses();
297					$aAliasNames = array_keys($aAliases);
298					$sRootCauseAlias = $aAliasNames[1]; // 1st column (=0) = object, second column = root cause
299					$oSet = new DBObjectSet($aContextQuery['search'], array(), array('id' => $oObj->GetKey()));
300					$oSet->OptimizeColumnLoad(array($aAliasNames[0] => array(), $aAliasNames[1] => array())); // Do not load any column... better do a reload than many joins
301					while($aRow = $oSet->FetchAssoc())
302					{
303						if (!is_null($aRow[$sRootCauseAlias]))
304						{
305							if (!array_key_exists($aContextQuery['key'], $aRootCauses))
306							{
307								$aRootCauses[$aContextQuery['key']] = array();
308							}
309							$aRootCauses[$aContextQuery['key']][] = $aRow[$sRootCauseAlias];
310							$bRet = true;
311						}
312					}
313				}
314			}
315		}
316		return $bRet;
317	}
318
319	/**
320	 * Build the graph downstream, and mark the nodes that can be reached from the source node
321	 *
322	 * @param string $sRelCode
323	 * @param int $iMaxDepth
324	 * @param bool $bEnableRedundancy
325	 * @param array $aUnreachableObjects
326	 *
327	 * @throws \CoreException
328	 * @throws \Exception
329	 */
330	public function ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachableObjects = array())
331	{
332		//echo "<h5>Sources only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
333		// Build the graph out of the sources
334		foreach ($this->aSourceNodes as $oSourceNode)
335		{
336			$this->AddRelatedObjects($sRelCode, true, $oSourceNode, $iMaxDepth, $bEnableRedundancy);
337			//echo "<h5>After processing of {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
338		}
339
340		// Mark the unreachable nodes
341		foreach ($aUnreachableObjects as $oObj)
342		{
343			$sNodeId = RelationObjectNode::MakeId($oObj);
344			$oNode = $this->GetNode($sNodeId);
345			if($oNode)
346			{
347				$oNode->SetProperty('is_reached_allowed', false);
348			}
349		}
350
351		// Determine the reached nodes
352		foreach ($this->aSourceNodes as $oSourceNode)
353		{
354			$oSourceNode->ReachDown('is_reached', true);
355			//echo "<h5>After reaching from {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
356		}
357
358		// Mark also the "context" nodes as reached and record the "root causes" for each node
359		$oIterator = new RelationTypeIterator($this, 'Node');
360		foreach($oIterator as $oNode)
361		{
362			$oObj = $oNode->GetProperty('object');
363			$aRootCauses = array();
364			if (!is_null($oObj) && $this->IsPartOfContext($oObj, $aRootCauses))
365			{
366				$oNode->SetProperty('context_root_causes', $aRootCauses);
367				$oNode->ReachDown('is_reached', true);
368			}
369		}
370	}
371
372	/**
373	 * Build the graph upstream
374	 *
375	 * @param string $sRelCode
376	 * @param int $iMaxDepth
377	 * @param bool $bEnableRedundancy
378	 *
379	 * @throws \CoreException
380	 * @throws \Exception
381	 */
382	public function ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy)
383	{
384		//echo "<h5>Sinks only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
385		// Build the graph out of the sinks
386		foreach ($this->aSinkNodes as $oSinkNode)
387		{
388			$this->AddRelatedObjects($sRelCode, false, $oSinkNode, $iMaxDepth, $bEnableRedundancy);
389			//echo "<h5>After processing of {$oSinkNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n";
390		}
391
392		// Mark also the "context" nodes as reached and record the "root causes" for each node
393		$oIterator = new RelationTypeIterator($this, 'Node');
394		foreach($oIterator as $oNode)
395		{
396			$oObj = $oNode->GetProperty('object');
397			$aRootCauses = array();
398			if (!is_null($oObj) && $this->IsPartOfContext($oObj, $aRootCauses))
399			{
400				$oNode->SetProperty('context_root_causes', $aRootCauses);
401				$oNode->ReachDown('is_reached', true);
402			}
403		}
404	}
405
406
407	/**
408	 * Recursively find related objects, and add them into the graph
409	 *
410	 * @param string $sRelCode The code of the relation to use for the computation
411	 * @param boolean $bDown The direction: downstream or upstream
412	 * @param \GraphElement $oObjectNode The node from which to compute the neighbours
413	 * @param int $iMaxDepth
414	 * @param boolean $bEnableRedundancy
415	 *
416	 * @throws \Exception
417	 */
418	protected function AddRelatedObjects($sRelCode, $bDown, $oObjectNode, $iMaxDepth, $bEnableRedundancy)
419	{
420		if ($iMaxDepth > 0)
421		{
422			if ($oObjectNode instanceof RelationRedundancyNode)
423			{
424				// Note: this happens when recursing on an existing part of the graph
425				// Skip that redundancy node
426				$aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges();
427				foreach ($aRelatedEdges as $oRelatedEdge)
428				{
429					$oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode();
430					// Recurse (same depth)
431					$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth, $bEnableRedundancy);
432				}
433			}
434			elseif ($oObjectNode->GetProperty('developped', false))
435			{
436				// No need to explore the underlying graph at all. We can stop here since the node has already been developped.
437				// Otherwise in case of "loops" in the graph we would recurse up to the max depth limit
438				// without producing any difference in the resulting graph... but potentially taking a LOOOONG time.
439				return;
440
441				// Former code was
442				//$aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges();
443				//foreach ($aRelatedEdges as $oRelatedEdge)
444				//{
445				//	$oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode();
446				//	// Recurse (decrement the depth)
447				//	$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
448				//}
449			}
450			else
451			{
452				$oObjectNode->SetProperty('developped', true);
453
454				$oObject = $oObjectNode->GetProperty('object');
455				$iPreviousTimeLimit = ini_get('max_execution_time');
456				$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
457				foreach (MetaModel::EnumRelationQueries(get_class($oObject), $sRelCode, $bDown) as $sDummy => $aQueryInfo)
458				{
459	 				$sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp'];
460					try
461					{
462						$oFlt = static::MakeSearch($sQuery);
463						$oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery());
464						$oRelatedObj = $oObjSet->Fetch();
465					}
466					catch (Exception $e)
467					{
468						$sDirection = $bDown ? 'downstream' : 'upstream';
469						throw new Exception("Wrong query ($sDirection) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
470					}
471					if ($oRelatedObj)
472					{
473						do
474						{
475							set_time_limit($iLoopTimeLimit);
476
477							$sObjectRef = 	RelationObjectNode::MakeId($oRelatedObj);
478							$oRelatedNode = $this->GetNode($sObjectRef);
479							if (is_null($oRelatedNode))
480							{
481								$oRelatedNode = new RelationObjectNode($this, $oRelatedObj);
482							}
483							$oSourceNode = $bDown ? $oObjectNode : $oRelatedNode;
484							$oSinkNode = $bDown ? $oRelatedNode : $oObjectNode;
485							if ($bEnableRedundancy)
486							{
487								$oRedundancyNode = $this->ComputeRedundancy($sRelCode, $aQueryInfo, $oSourceNode, $oSinkNode);
488							}
489							else
490							{
491								$oRedundancyNode = null;
492							}
493							if (!$oRedundancyNode)
494							{
495								// Direct link (otherwise handled by ComputeRedundancy)
496								new RelationEdge($this, $oSourceNode, $oSinkNode);
497							}
498							// Recurse
499							$this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy);
500						}
501						while ($oRelatedObj = $oObjSet->Fetch());
502					}
503				}
504				set_time_limit($iPreviousTimeLimit);
505			}
506		}
507	}
508
509	/**
510	 * Determine if there is a redundancy (or use the existing one) and add the corresponding nodes/edges
511	 *
512	 * @param string $sRelCode
513	 * @param array $aQueryInfo
514	 * @param GraphElement $oFromNode
515	 * @param GraphElement $oToNode
516	 *
517	 * @return \GraphNode|NULL|\RelationRedundancyNode
518	 * @throws \Exception
519	 */
520	protected function ComputeRedundancy($sRelCode, $aQueryInfo, $oFromNode, $oToNode)
521	{
522		$oRedundancyNode = null;
523		$oObject = $oToNode->GetProperty('object');
524		if ($this->IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode))
525		{
526			$sUniqueNeighbourId = $aQueryInfo['sDefinedInClass'].'-'.$aQueryInfo['sNeighbour'];
527			$sId = RelationRedundancyNode::MakeId($sRelCode, $sUniqueNeighbourId, $oFromNode->GetProperty('object'), $oToNode->GetProperty('object'));
528
529			$oRedundancyNode = $this->GetNode($sId);
530			if (is_null($oRedundancyNode))
531			{
532				// Get the upper neighbours
533				$sQuery = $aQueryInfo['sQueryUp'];
534				if (!$sQuery)
535				{
536					throw new Exception("Redundancy cannot be enabled on the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: its direction is \"{$aQueryInfo['sDirection']}\"");
537				}
538				try
539				{
540					$oFlt = static::MakeSearch($sQuery);
541					$oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery());
542					$iCount = $oObjSet->Count();
543				}
544				catch (Exception $e)
545				{
546					throw new Exception("Wrong query (upstream) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage());
547				}
548
549				$iMinUp = $this->GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iCount);
550				$fThreshold = max(0, $iCount - $iMinUp);
551				$oRedundancyNode = new RelationRedundancyNode($this, $sId, $iMinUp, $fThreshold);
552				new RelationEdge($this, $oRedundancyNode, $oToNode);
553
554				while ($oUpperObj = $oObjSet->Fetch())
555				{
556					$sObjectRef = 	RelationObjectNode::MakeId($oUpperObj);
557					$oUpperNode = $this->GetNode($sObjectRef);
558					if (is_null($oUpperNode))
559					{
560						$oUpperNode = new RelationObjectNode($this, $oUpperObj);
561					}
562					new RelationEdge($this, $oUpperNode, $oRedundancyNode);
563				}
564			}
565		}
566		return $oRedundancyNode;
567	}
568
569	/**
570	 * Helper to determine the redundancy setting on a given relation
571	 *
572	 * @param string $sRelCode
573	 * @param array $aQueryInfo
574	 * @param GraphElement $oToNode
575	 *
576	 * @return bool
577	 */
578	protected function IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode)
579	{
580		$bRet = false;
581		$oToObject = $oToNode->GetProperty('object');
582		$oRedundancyAttDef = $this->FindRedundancyAttribute($sRelCode, $aQueryInfo, get_class($oToObject));
583		if ($oRedundancyAttDef)
584		{
585			$sValue = $oToObject->Get($oRedundancyAttDef->GetCode());
586			$bRet = $oRedundancyAttDef->IsEnabled($sValue);
587		}
588		return $bRet;
589	}
590
591	/**
592	 * Helper to determine the redundancy threshold, given the count of objects upstream
593	 *
594	 * @param string $sRelCode
595	 * @param array $aQueryInfo
596	 * @param GraphElement $oToNode
597	 * @param int $iUpstreamObjects
598	 *
599	 * @return int
600	 */
601	protected function GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iUpstreamObjects)
602	{
603		$iMinUp = 0;
604
605		$oToObject = $oToNode->GetProperty('object');
606		$oRedundancyAttDef = $this->FindRedundancyAttribute($sRelCode, $aQueryInfo, get_class($oToObject));
607		if ($oRedundancyAttDef)
608		{
609			$sValue = $oToObject->Get($oRedundancyAttDef->GetCode());
610			if ($oRedundancyAttDef->GetMinUpType($sValue) == 'count')
611			{
612				$iMinUp = $oRedundancyAttDef->GetMinUpValue($sValue);
613			}
614			else
615			{
616				$iMinUp = $iUpstreamObjects * $oRedundancyAttDef->GetMinUpValue($sValue) / 100;
617			}
618		}
619		return $iMinUp;
620	}
621
622	/**
623	 * Helper to search for the redundancy attribute
624	 *
625	 * @param string $sRelCode
626	 * @param array $aQueryInfo
627	 * @param string $sClass
628	 *
629	 * @return \AttributeDefinition|\AttributeRedundancySettings|null
630	 */
631	protected function FindRedundancyAttribute($sRelCode, $aQueryInfo, $sClass)
632	{
633		$oRet = null;
634		foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
635		{
636			if ($oAttDef instanceof AttributeRedundancySettings)
637			{
638				if ($oAttDef->Get('relation_code') == $sRelCode)
639				{
640					if ($oAttDef->Get('from_class') == $aQueryInfo['sFromClass'])
641					{
642						if ($oAttDef->Get('neighbour_id') == $aQueryInfo['sNeighbour'])
643						{
644							$oRet = $oAttDef;
645							break;
646						}
647					}
648				}
649			}
650		}
651		return $oRet;
652	}
653
654	/**
655	 * Get the objects referenced by the graph as a hash array: 'class' => array of objects
656	 * @return array Ambigous <multitype:multitype: , unknown>
657	 */
658	public function GetObjectsByClass()
659	{
660		$aResults = array();
661		$oIterator = new RelationTypeIterator($this, 'Node');
662		foreach($oIterator as $oNode)
663		{
664			$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object
665			if ($oObj)
666			{
667				$sObjClass  = get_class($oObj);
668				if (!array_key_exists($sObjClass, $aResults))
669				{
670					$aResults[$sObjClass] = array();
671				}
672				$aResults[$sObjClass][] = $oObj;
673			}
674		}
675		return $aResults;
676	}
677
678	/**
679	 * @param string $sOQL
680	 *
681	 * @return \DBSearch
682	 * @throws \CoreException
683	 * @throws \OQLException
684	 */
685	protected static function MakeSearch($sOQL)
686	{
687		$oSearch = DBSearch::FromOQL($sOQL);
688		if (MetaModel::IsObsoletable($oSearch->GetClass()))
689		{
690			// Exclude obsolete objects anytime
691			$oSearch->AddCondition('obsolescence_flag', 0);
692		}
693		// Exclude archived objects anytime
694		$oSearch->SetArchiveMode(false);
695		return $oSearch;
696	}
697}
698