1<?php
2// Copyright (c) 2010-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// Dev hack for disabling the some query build optimizations (Folding/Merging)
21define('ENABLE_OPT', true);
22
23class DBObjectSearch extends DBSearch
24{
25	private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters)
26	private $m_aSelectedClasses; // selected for the output (alias => class name)
27	private $m_oSearchCondition;
28	private $m_aParams;
29	private $m_aPointingTo;
30	private $m_aReferencedBy;
31
32	// By default, some information may be hidden to the current user
33	// But it may happen that we need to disable that feature
34	protected $m_bAllowAllData = false;
35	protected $m_bDataFiltered = false;
36
37	public function __construct($sClass, $sClassAlias = null)
38	{
39		parent::__construct();
40
41		if (is_null($sClassAlias)) $sClassAlias = $sClass;
42		if(!is_string($sClass)) throw new Exception('DBObjectSearch::__construct called with a non-string parameter: $sClass = '.print_r($sClass, true));
43		if(!MetaModel::IsValidClass($sClass)) throw new Exception('DBObjectSearch::__construct called for an invalid class: "'.$sClass.'"');
44
45		$this->m_aSelectedClasses = array($sClassAlias => $sClass);
46		$this->m_aClasses = array($sClassAlias => $sClass);
47		$this->m_oSearchCondition = new TrueExpression;
48		$this->m_aParams = array();
49		$this->m_aPointingTo = array();
50		$this->m_aReferencedBy = array();
51	}
52
53	public function AllowAllData($bAllowAllData = true) {$this->m_bAllowAllData = $bAllowAllData;}
54	public function IsAllDataAllowed() {return $this->m_bAllowAllData;}
55	protected function IsDataFiltered() {return $this->m_bDataFiltered; }
56	protected function SetDataFiltered() {$this->m_bDataFiltered = true;}
57
58	// Create a search definition that leads to 0 result, still a valid search object
59	static public function FromEmptySet($sClass)
60	{
61		$oResultFilter = new DBObjectSearch($sClass);
62		$oResultFilter->m_oSearchCondition = new FalseExpression;
63		return $oResultFilter;
64	}
65
66
67	public function GetJoinedClasses() {return $this->m_aClasses;}
68
69	public function GetClassName($sAlias)
70	{
71		if (array_key_exists($sAlias, $this->m_aSelectedClasses))
72		{
73			return $this->m_aSelectedClasses[$sAlias];
74		}
75		else
76		{
77			throw new CoreException("Invalid class alias '$sAlias'");
78		}
79	}
80
81	public function GetClass()
82	{
83		return reset($this->m_aSelectedClasses);
84	}
85	public function GetClassAlias()
86	{
87		reset($this->m_aSelectedClasses);
88		return key($this->m_aSelectedClasses);
89	}
90
91	public function GetFirstJoinedClass()
92	{
93		return reset($this->m_aClasses);
94	}
95	public function GetFirstJoinedClassAlias()
96	{
97		reset($this->m_aClasses);
98		return key($this->m_aClasses);
99	}
100
101	/**
102	 * Change the class (only subclasses are supported as of now, because the conditions must fit the new class)
103	 * Defaults to the first selected class (most of the time it is also the first joined class
104	 *
105	 * @param $sNewClass
106	 * @param null $sAlias
107	 *
108	 * @throws \CoreException
109	 */
110	public function ChangeClass($sNewClass, $sAlias = null)
111	{
112		if (is_null($sAlias))
113		{
114			$sAlias = $this->GetClassAlias();
115		}
116		else
117		{
118			if (!array_key_exists($sAlias, $this->m_aSelectedClasses))
119			{
120				// discard silently - necessary when recursing on the related nodes (see code below)
121				return;
122			}
123		}
124		$sCurrClass = $this->GetClassName($sAlias);
125		if ($sNewClass == $sCurrClass)
126		{
127			// Skip silently
128			return;
129		}
130		if (!MetaModel::IsParentClass($sCurrClass, $sNewClass))
131		{
132			throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted.");
133		}
134
135		// Change for this node
136		//
137		$this->m_aSelectedClasses[$sAlias] = $sNewClass;
138		$this->m_aClasses[$sAlias] = $sNewClass;
139
140		// Change for all the related node (yes, this was necessary with some queries - strange effects otherwise)
141		//
142		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
143		{
144			foreach($aPointingTo as $iOperatorCode => $aFilter)
145			{
146				foreach($aFilter as $oExtFilter)
147				{
148					$oExtFilter->ChangeClass($sNewClass, $sAlias);
149				}
150			}
151		}
152		foreach($this->m_aReferencedBy as $sForeignClass => $aReferences)
153		{
154			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
155			{
156				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
157				{
158					foreach ($aFilters as $oForeignFilter)
159					{
160						$oForeignFilter->ChangeClass($sNewClass, $sAlias);
161					}
162				}
163			}
164		}
165	}
166
167	public function GetSelectedClasses()
168	{
169		return $this->m_aSelectedClasses;
170	}
171
172	/**
173	 * @param array $aSelectedClasses array of aliases
174	 * @throws CoreException
175	 */
176	public function SetSelectedClasses($aSelectedClasses)
177	{
178		$this->m_aSelectedClasses = array();
179		foreach ($aSelectedClasses as $sAlias)
180		{
181			if (!array_key_exists($sAlias, $this->m_aClasses))
182			{
183				throw new CoreException("SetSelectedClasses: Invalid class alias $sAlias");
184			}
185			$this->m_aSelectedClasses[$sAlias] = $this->m_aClasses[$sAlias];
186		}
187	}
188
189	/**
190	 * Change any alias of the query tree
191	 *
192	 * @param $sOldName
193	 * @param $sNewName
194	 *
195	 * @return bool True if the alias has been found and changed
196	 * @throws \Exception
197	 */
198	public function RenameAlias($sOldName, $sNewName)
199	{
200		$bFound = false;
201		if (array_key_exists($sOldName, $this->m_aClasses))
202		{
203			$bFound = true;
204		}
205		if (array_key_exists($sNewName, $this->m_aClasses))
206		{
207			throw new Exception("RenameAlias: alias '$sNewName' already used");
208		}
209
210		$aClasses = array();
211		foreach ($this->m_aClasses as $sAlias => $sClass)
212		{
213			if ($sAlias === $sOldName)
214			{
215				$aClasses[$sNewName] = $sClass;
216			}
217			else
218			{
219				$aClasses[$sAlias] = $sClass;
220			}
221		}
222		$this->m_aClasses = $aClasses;
223
224		$aSelectedClasses = array();
225		foreach ($this->m_aSelectedClasses as $sAlias => $sClass)
226		{
227			if ($sAlias === $sOldName)
228			{
229				$aSelectedClasses[$sNewName] = $sClass;
230			}
231			else
232			{
233				$aSelectedClasses[$sAlias] = $sClass;
234			}
235		}
236		$this->m_aSelectedClasses = $aSelectedClasses;
237
238		$this->m_oSearchCondition->RenameAlias($sOldName, $sNewName);
239
240		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
241		{
242			foreach($aPointingTo as $iOperatorCode => $aFilter)
243			{
244				foreach($aFilter as $oExtFilter)
245				{
246					$bFound = $oExtFilter->RenameAlias($sOldName, $sNewName) || $bFound;
247				}
248			}
249		}
250		foreach($this->m_aReferencedBy as $sForeignClass => $aReferences)
251		{
252			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
253			{
254				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
255				{
256					foreach ($aFilters as $oForeignFilter)
257					{
258						$bFound = $oForeignFilter->RenameAlias($sOldName, $sNewName) || $bFound;
259					}
260				}
261			}
262		}
263		return $bFound;
264	}
265
266	public function SetModifierProperty($sPluginClass, $sProperty, $value)
267	{
268		$this->m_aModifierProperties[$sPluginClass][$sProperty] = $value;
269	}
270
271	public function GetModifierProperties($sPluginClass)
272	{
273		if (array_key_exists($sPluginClass, $this->m_aModifierProperties))
274		{
275			return $this->m_aModifierProperties[$sPluginClass];
276		}
277		else
278		{
279			return array();
280		}
281	}
282
283	public function IsAny()
284	{
285		if (!$this->m_oSearchCondition->IsTrue()) return false;
286		if (count($this->m_aPointingTo) > 0) return false;
287		if (count($this->m_aReferencedBy) > 0) return false;
288		return true;
289	}
290
291	protected function TransferConditionExpression($oFilter, $aTranslation)
292	{
293		// Prevent collisions in the parameter names by renaming them if needed
294		foreach($this->m_aParams as $sParam => $value)
295		{
296			if (array_key_exists($sParam, $oFilter->m_aParams) && ($value != $oFilter->m_aParams[$sParam]))
297			{
298				// Generate a new and unique name for the collinding parameter
299				$index = 1;
300				while(array_key_exists($sParam.$index, $oFilter->m_aParams))
301				{
302					$index++;
303				}
304				$secondValue = $oFilter->m_aParams[$sParam];
305				$oFilter->RenameParam($sParam, $sParam.$index);
306				unset($oFilter->m_aParams[$sParam]);
307				$oFilter->m_aParams[$sParam.$index] = $secondValue;
308			}
309		}
310		$oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */);
311		$this->AddConditionExpression($oTranslated);
312		$this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams);
313	}
314
315	protected function RenameParam($sOldName, $sNewName)
316	{
317		$this->m_oSearchCondition->RenameParam($sOldName, $sNewName);
318		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
319		{
320			foreach($aPointingTo as $iOperatorCode => $aFilter)
321			{
322				foreach($aFilter as $oExtFilter)
323				{
324					$oExtFilter->RenameParam($sOldName, $sNewName);
325				}
326			}
327		}
328		foreach($this->m_aReferencedBy as $sForeignClass => $aReferences)
329		{
330			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
331			{
332				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
333				{
334					foreach ($aFilters as $oForeignFilter)
335					{
336						$oForeignFilter->RenameParam($sOldName, $sNewName);
337					}
338				}
339			}
340		}
341	}
342
343	public function ResetCondition()
344	{
345		$this->m_oSearchCondition = new TrueExpression();
346		// ? is that usefull/enough, do I need to rebuild the list after the subqueries ?
347	}
348
349	public function MergeConditionExpression($oExpression)
350	{
351		$this->m_oSearchCondition = $this->m_oSearchCondition->LogOr($oExpression);
352	}
353
354	public function AddConditionExpression($oExpression)
355	{
356		$this->m_oSearchCondition = $this->m_oSearchCondition->LogAnd($oExpression);
357	}
358
359  	public function AddNameCondition($sName)
360	{
361		$oValueExpr = new ScalarExpression($sName);
362		$oNameExpr = new FieldExpression('friendlyname', $this->GetClassAlias());
363		$oNewCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr);
364		$this->AddConditionExpression($oNewCondition);
365	}
366
367	/**
368	 * @param string $sFilterCode
369	 * @param mixed $value
370	 * @param string $sOpCode operator to use : 'IN', 'NOT IN', 'Contains',' Begins with', 'Finishes with', ...
371	 * @param bool $bParseSearchString
372	 *
373	 * @throws \CoreException
374	 *
375	 * @see AddConditionForInOperatorUsingParam for IN/NOT IN queries with lots of params
376	 */
377	public function AddCondition($sFilterCode, $value, $sOpCode = null, $bParseSearchString = false)
378	{
379		MyHelpers::CheckKeyInArray('filter code in class: '.$this->GetClass(), $sFilterCode, MetaModel::GetClassFilterDefs($this->GetClass()));
380
381		$oField = new FieldExpression($sFilterCode, $this->GetClassAlias());
382		if (empty($sOpCode))
383		{
384			if ($sFilterCode == 'id')
385			{
386				$sOpCode = '=';
387			}
388			else
389			{
390				$oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode);
391				$oNewCondition = $oAttDef->GetSmartConditionExpression($value, $oField, $this->m_aParams);
392				$this->AddConditionExpression($oNewCondition);
393				return;
394			}
395		}
396		// Parse search strings if needed and if the filter code corresponds to a valid attcode
397		if($bParseSearchString && MetaModel::IsValidAttCode($this->GetClass(), $sFilterCode))
398		{
399			$oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode);
400			$value = $oAttDef->ParseSearchString($value);
401		}
402
403		// Preserve backward compatibility - quick n'dirty way to change that API semantic
404		//
405		switch($sOpCode)
406		{
407		case 'SameDay':
408		case 'SameMonth':
409		case 'SameYear':
410		case 'Today':
411		case '>|':
412		case '<|':
413		case '=|':
414			throw new CoreException('Deprecated operator, please consider using OQL (SQL) expressions like "(TO_DAYS(NOW()) - TO_DAYS(x)) AS AgeDays"', array('operator' => $sOpCode));
415			break;
416
417		case 'IN':
418			if (!is_array($value)) $value = array($value);
419			if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.');
420			$sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')';
421			$sOQLCondition = $oField->Render()." IN $sListExpr";
422			break;
423
424		case 'NOTIN':
425			if (!is_array($value)) $value = array($value);
426            if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.');
427			$sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')';
428			$sOQLCondition = $oField->Render()." NOT IN $sListExpr";
429			break;
430
431		case 'Contains':
432			$this->m_aParams[$sFilterCode] = "%$value%";
433			$sOperator = 'LIKE';
434			break;
435
436		case 'Begins with':
437			$this->m_aParams[$sFilterCode] = "$value%";
438			$sOperator = 'LIKE';
439			break;
440
441		case 'Finishes with':
442			$this->m_aParams[$sFilterCode] = "%$value";
443			$sOperator = 'LIKE';
444			break;
445
446		default:
447			if ($value === null)
448			{
449				switch ($sOpCode)
450				{
451					case '=':
452						$sOpCode = '*Expression*';
453						$oExpression = new FunctionExpression('ISNULL', array($oField));
454						break;
455					case '!=':
456						$sOpCode = '*Expression*';
457						$oExpression = new FunctionExpression('ISNULL', array($oField));
458						$oExpression = new BinaryExpression($oExpression, '=', new ScalarExpression(0));
459						break;
460					default:
461						throw new Exception("AddCondition on null value: unsupported operator '$sOpCode''");
462				}
463			}
464			else
465			{
466				$this->m_aParams[$sFilterCode] = $value;
467				$sOperator = $sOpCode;
468			}
469		}
470
471		switch($sOpCode)
472		{
473			case '*Expression*':
474				$oNewCondition = $oExpression;
475				break;
476			case "IN":
477			case "NOTIN":
478				// this will parse all of the values... Can take forever if there are lots of them !
479				// In this case using a parameter is far better : WHERE ... IN (:my_param)
480				$oNewCondition = Expression::FromOQL($sOQLCondition);
481				break;
482
483			case 'MATCHES':
484				$oRightExpr = new ScalarExpression($value);
485				$oNewCondition = new MatchExpression($oField, $oRightExpr);
486				break;
487
488			case 'Contains':
489			case 'Begins with':
490			case 'Finishes with':
491			default:
492				$oRightExpr = new VariableExpression($sFilterCode);
493				$oNewCondition = new BinaryExpression($oField, $sOperator, $oRightExpr);
494		}
495
496		$this->AddConditionExpression($oNewCondition);
497	}
498
499	/**
500	 * @param string $sFilterCode attribute code to use
501	 * @param array $aValues
502	 * @param bool $bPositiveMatch if true will add a IN filter, else a NOT IN
503	 *
504	 * @throws \CoreException
505	 *
506	 * @since 2.5 N°1418
507	 */
508	public function AddConditionForInOperatorUsingParam($sFilterCode, $aValues, $bPositiveMatch = true)
509	{
510		$oFieldExpression = new FieldExpression($sFilterCode, $this->GetClassAlias());
511
512		$sOperator = $bPositiveMatch ? 'IN' : 'NOT IN';
513
514		$sInParamName = $this->GenerateUniqueParamName();
515		$oParamExpression = new VariableExpression($sInParamName);
516		$this->GetInternalParamsByRef()[$sInParamName] = $aValues;
517
518		$oListExpression = new ListExpression(array($oParamExpression));
519
520		$oInCondition = new BinaryExpression($oFieldExpression, $sOperator, $oListExpression);
521		$this->AddConditionExpression($oInCondition);
522	}
523
524	/**
525	 * Specify a condition on external keys or link sets
526	 * @param string $sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively
527	 *                 Example: infra_list->ci_id->location_id->country
528	 * @param $value
529	 * @return void
530	 * @throws \CoreException
531	 * @throws \CoreWarning
532	 */
533	public function AddConditionAdvanced($sAttSpec, $value)
534	{
535		$sClass = $this->GetClass();
536
537		$iPos = strpos($sAttSpec, '->');
538		if ($iPos !== false)
539		{
540			$sAttCode = substr($sAttSpec, 0, $iPos);
541			$sSubSpec = substr($sAttSpec, $iPos + 2);
542
543			if (!MetaModel::IsValidAttCode($sClass, $sAttCode))
544			{
545				throw new Exception("Invalid attribute code '$sClass/$sAttCode' in condition specification '$sAttSpec'");
546			}
547
548			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
549			if ($oAttDef->IsLinkSet())
550			{
551				$sTargetClass = $oAttDef->GetLinkedClass();
552				$sExtKeyToMe = $oAttDef->GetExtKeyToMe();
553
554				$oNewFilter = new DBObjectSearch($sTargetClass);
555				$oNewFilter->AddConditionAdvanced($sSubSpec, $value);
556
557				$this->AddCondition_ReferencedBy($oNewFilter, $sExtKeyToMe);
558			}
559			elseif ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
560			{
561				$sTargetClass = $oAttDef->GetTargetClass(EXTKEY_ABSOLUTE);
562
563				$oNewFilter = new DBObjectSearch($sTargetClass);
564				$oNewFilter->AddConditionAdvanced($sSubSpec, $value);
565
566				$this->AddCondition_PointingTo($oNewFilter, $sAttCode);
567			}
568			else
569			{
570				throw new Exception("Attribute specification '$sAttSpec', '$sAttCode' should be either a link set or an external key");
571			}
572		}
573		else
574		{
575			// $sAttSpec is an attribute code
576			//
577			if (is_array($value))
578			{
579				$oField = new FieldExpression($sAttSpec, $this->GetClass());
580				$oListExpr = ListExpression::FromScalars($value);
581				$oInValues = new BinaryExpression($oField, 'IN', $oListExpr);
582
583				$this->AddConditionExpression($oInValues);
584			}
585			else
586			{
587				$this->AddCondition($sAttSpec, $value);
588			}
589		}
590	}
591
592	public function AddCondition_FullText($sNeedle)
593	{
594		// Transform the full text condition into additional condition expression
595		$aFullTextFields = array();
596		foreach (MetaModel::ListAttributeDefs($this->GetClass()) as $sAttCode => $oAttDef)
597		{
598			if (!$oAttDef->IsScalar()) continue;
599			if ($oAttDef->IsExternalKey()) continue;
600			$aFullTextFields[] = new FieldExpression($sAttCode, $this->GetClassAlias());
601		}
602		$oTextFields = new CharConcatWSExpression(' ', $aFullTextFields);
603
604		$sQueryParam = 'needle';
605		$oFlexNeedle = new CharConcatExpression(array(new ScalarExpression('%'), new VariableExpression($sQueryParam), new ScalarExpression('%')));
606
607		$oNewCond = new BinaryExpression($oTextFields, 'LIKE', $oFlexNeedle);
608		$this->AddConditionExpression($oNewCond);
609		$this->m_aParams[$sQueryParam] = $sNeedle;
610	}
611
612	protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true)
613	{
614		if ($bTranslateMainAlias)
615		{
616			$sOrigAlias = $this->GetFirstJoinedClassAlias();
617			if (array_key_exists($sOrigAlias, $aClassAliases))
618			{
619				$sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetFirstJoinedClass());
620				if (isset($this->m_aSelectedClasses[$sOrigAlias]))
621				{
622					$this->m_aSelectedClasses[$sNewAlias] = $this->GetFirstJoinedClass();
623					unset($this->m_aSelectedClasses[$sOrigAlias]);
624				}
625
626				// TEMPORARY ALGORITHM (m_aClasses is not correctly updated, it is not possible to add a subtree onto a subnode)
627				// Replace the element at the same position (unset + set is not enough because the hash array is ordered)
628				$aPrevList = $this->m_aClasses;
629				$this->m_aClasses = array();
630				foreach ($aPrevList as $sSomeAlias => $sSomeClass)
631				{
632					if ($sSomeAlias == $sOrigAlias)
633					{
634						$this->m_aClasses[$sNewAlias] = $sSomeClass; // note: GetFirstJoinedClass now returns '' !!!
635					}
636					else
637					{
638						$this->m_aClasses[$sSomeAlias] = $sSomeClass;
639					}
640				}
641
642				// Translate the condition expression with the new alias
643				$aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias;
644			}
645
646			// add the alias into the filter aliases list
647			$aClassAliases[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass();
648		}
649
650		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
651		{
652			foreach($aPointingTo as $iOperatorCode => $aFilter)
653			{
654				foreach($aFilter as $oFilter)
655				{
656					$oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
657				}
658			}
659		}
660
661		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
662		{
663			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
664			{
665				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
666				{
667					foreach ($aFilters as $oForeignFilter)
668					{
669						$oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
670					}
671				}
672			}
673		}
674	}
675
676
677	// Browse the tree nodes recursively
678	//
679	protected function GetNode($sAlias)
680	{
681		if ($this->GetFirstJoinedClassAlias() == $sAlias)
682		{
683			return $this;
684		}
685		else
686		{
687			foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
688			{
689				foreach($aPointingTo as $iOperatorCode => $aFilter)
690				{
691					foreach($aFilter as $oFilter)
692					{
693						$ret = $oFilter->GetNode($sAlias);
694						if (is_object($ret))
695						{
696							return $ret;
697						}
698					}
699				}
700			}
701			foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
702			{
703				foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
704				{
705					foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
706					{
707						foreach ($aFilters as $oForeignFilter)
708						{
709							$ret = $oForeignFilter->GetNode($sAlias);
710							if (is_object($ret))
711							{
712								return $ret;
713							}
714						}
715					}
716				}
717			}
718		}
719		// Not found
720		return null;
721	}
722
723	/**
724	 * Helper to
725	 * - convert a translation table (format optimized for the translation in an expression tree) into simple hash
726	 * - compile over an eventually existing map
727	 *
728	 * @param array $aRealiasingMap Map to update
729	 * @param array $aAliasTranslation Translation table resulting from calls to MergeWith_InNamespace
730	 * @return void of <old-alias> => <new-alias>
731	 */
732	protected function UpdateRealiasingMap(&$aRealiasingMap, $aAliasTranslation)
733	{
734		if ($aRealiasingMap !== null)
735		{
736			foreach ($aAliasTranslation as $sPrevAlias => $aRules)
737			{
738				if (isset($aRules['*']))
739				{
740					$sNewAlias = $aRules['*'];
741					$sOriginalAlias = array_search($sPrevAlias, $aRealiasingMap);
742					if ($sOriginalAlias !== false)
743					{
744						$aRealiasingMap[$sOriginalAlias] = $sNewAlias;
745					}
746					else
747					{
748						$aRealiasingMap[$sPrevAlias] = $sNewAlias;
749					}
750				}
751			}
752		}
753	}
754
755	/**
756	 * Completes the list of alias=>class by browsing the whole structure recursively
757	 * This a workaround to handle some cases in which the list of classes is not correctly updated.
758	 * This code should disappear as soon as DBObjectSearch get split between a container search class and a Node class
759	 *
760	 * @param array $aClasses List to be completed
761	 */
762	protected function RecomputeClassList(&$aClasses)
763	{
764		$aClasses[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass();
765
766		// Recurse in the query tree
767		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
768		{
769			foreach($aPointingTo as $iOperatorCode => $aFilter)
770			{
771				foreach($aFilter as $oFilter)
772				{
773					$oFilter->RecomputeClassList($aClasses);
774				}
775			}
776		}
777
778		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
779		{
780			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
781			{
782				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
783				{
784					foreach ($aFilters as $oForeignFilter)
785					{
786						$oForeignFilter->RecomputeClassList($aClasses);
787					}
788				}
789			}
790		}
791	}
792
793	/**
794	 * @param DBObjectSearch $oFilter
795	 * @param $sExtKeyAttCode
796	 * @param int $iOperatorCode
797	 * @param null $aRealiasingMap array of <old-alias> => <new-alias>, for each alias that has changed
798	 * @throws CoreException
799	 * @throws CoreWarning
800	 */
801	public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null)
802	{
803		if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode))
804		{
805			throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}'");
806		}
807		$oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
808		if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass()))
809		{
810			throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
811		}
812		if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
813		{
814			throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
815		}
816		// Note: though it seems to be a good practice to clone the given source filter
817		//       (as it was done and fixed an issue in Intersect())
818		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
819		//       root cause: FromOQL relies on the fact that the passed filter can be modified later
820		// NO: $oFilter = $oFilter->DeepClone();
821		// See also: Trac #639, and self::AddCondition_ReferencedBy()
822		$aAliasTranslation = array();
823		$res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode);
824		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
825		$this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation);
826
827		if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass()))
828		{
829			if (isset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode]))
830			{
831				foreach ($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode] as $oRemoteFilter)
832				{
833					if ($this->GetClass() == $oRemoteFilter->GetClass())
834					{
835						// Optimization - fold sibling query
836						$aAliasTranslation = array();
837						$this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation);
838						unset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode]);
839						$this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false);
840						$this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation);
841						break;
842					}
843				}
844			}
845		}
846		$this->RecomputeClassList($this->m_aClasses);
847		return $res;
848	}
849
850	protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
851	{
852		// Find the node on which the new tree must be attached (most of the time it is "this")
853		$oReceivingFilter = $this->GetNode($this->GetClassAlias());
854
855		$bMerged = false;
856		if (ENABLE_OPT && isset($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode]))
857		{
858			foreach ($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] as $oExisting)
859			{
860				if ($oExisting->GetClass() == $oFilter->GetClass())
861				{
862					$oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation);
863					$bMerged = true;
864					break;
865				}
866			}
867		}
868		if (!$bMerged)
869		{
870			$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
871			$oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][] = $oFilter;
872		}
873	}
874
875	/**
876	 * @param DBObjectSearch $oFilter
877	 * @param $sForeignExtKeyAttCode
878	 * @param int $iOperatorCode
879	 * @param null $aRealiasingMap array of <old-alias> => <new-alias>, for each alias that has changed
880	 * @return void
881	 * @throws \CoreException
882	 */
883	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null)
884	{
885		$sForeignClass = $oFilter->GetClass();
886		if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode))
887		{
888			throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}'");
889		}
890		$oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
891		if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass()))
892		{
893			// à refaire en spécifique dans FromOQL
894			throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
895		}
896
897		// Note: though it seems to be a good practice to clone the given source filter
898		//       (as it was done and fixed an issue in Intersect())
899		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
900		//       root cause: FromOQL relies on the fact that the passed filter can be modified later
901		// NO: $oFilter = $oFilter->DeepClone();
902		// See also: Trac #639, and self::AddCondition_PointingTo()
903		$aAliasTranslation = array();
904		$this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode);
905		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
906		$this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation);
907
908		if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass()))
909		{
910			if (isset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode]))
911			{
912				foreach ($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode] as $oRemoteFilter)
913				{
914					if ($this->GetClass() == $oRemoteFilter->GetClass())
915					{
916						// Optimization - fold sibling query
917						$aAliasTranslation = array();
918						$this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation);
919						unset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode]);
920						$this->m_oSearchCondition  = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false);
921						$this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation);
922						break;
923					}
924				}
925			}
926		}
927		$this->RecomputeClassList($this->m_aClasses);
928	}
929
930	protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
931	{
932		$sForeignClass = $oFilter->GetClass();
933
934		// Find the node on which the new tree must be attached (most of the time it is "this")
935		$oReceivingFilter = $this->GetNode($this->GetClassAlias());
936
937		$bMerged = false;
938		if (ENABLE_OPT && isset($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode]))
939		{
940			foreach ($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode] as $oExisting)
941			{
942				if ($oExisting->GetClass() == $oFilter->GetClass())
943				{
944					$oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation);
945					$bMerged = true;
946					break;
947				}
948			}
949		}
950		if (!$bMerged)
951		{
952			$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
953			$oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode][] = $oFilter;
954		}
955	}
956
957	public function Intersect(DBSearch $oFilter)
958	{
959		if ($oFilter instanceof DBUnionSearch)
960		{
961			// Develop!
962			$aFilters = $oFilter->GetSearches();
963		}
964		else
965		{
966			$aFilters = array($oFilter);
967		}
968
969		$aSearches = array();
970		foreach ($aFilters as $oRightFilter)
971		{
972			// Limitation: the queried class must be the first declared class
973			if ($this->GetFirstJoinedClassAlias() != $this->GetClassAlias())
974			{
975				throw new CoreException("Limitation: cannot merge two queries if the queried class ({$this->GetClass()} AS {$this->GetClassAlias()}) is not the first joined class ({$this->GetFirstJoinedClass()} AS {$this->GetFirstJoinedClassAlias()})");
976			}
977			if ($oRightFilter->GetFirstJoinedClassAlias() != $oRightFilter->GetClassAlias())
978			{
979				throw new CoreException("Limitation: cannot merge two queries if the queried class ({$oRightFilter->GetClass()} AS {$oRightFilter->GetClassAlias()}) is not the first joined class ({$oRightFilter->GetFirstJoinedClass()} AS {$oRightFilter->GetFirstJoinedClassAlias()})");
980			}
981
982			$oLeftFilter = $this->DeepClone();
983			$oRightFilter = $oRightFilter->DeepClone();
984
985			$bAllowAllData = ($oLeftFilter->IsAllDataAllowed() && $oRightFilter->IsAllDataAllowed());
986			if ($bAllowAllData)
987			{
988				$oLeftFilter->AllowAllData();
989			}
990
991			if ($oLeftFilter->GetClass() != $oRightFilter->GetClass())
992			{
993				if (MetaModel::IsParentClass($oLeftFilter->GetClass(), $oRightFilter->GetClass()))
994				{
995					// Specialize $oLeftFilter
996					$oLeftFilter->ChangeClass($oRightFilter->GetClass());
997				}
998				elseif (MetaModel::IsParentClass($oRightFilter->GetClass(), $oLeftFilter->GetClass()))
999				{
1000					// Specialize $oRightFilter
1001					$oRightFilter->ChangeClass($oLeftFilter->GetClass());
1002				}
1003				else
1004				{
1005					throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'");
1006				}
1007			}
1008
1009			$aAliasTranslation = array();
1010			$oLeftFilter->MergeWith_InNamespace($oRightFilter, $oLeftFilter->m_aClasses, $aAliasTranslation);
1011			$oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation);
1012			$aSearches[] = $oLeftFilter;
1013		}
1014		if (count($aSearches) == 1)
1015		{
1016			// return a DBObjectSearch
1017			return $aSearches[0];
1018		}
1019		else
1020		{
1021			return new DBUnionSearch($aSearches);
1022		}
1023	}
1024
1025	protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation)
1026	{
1027		if ($this->GetClass() != $oFilter->GetClass())
1028		{
1029			throw new CoreException("Attempting to merge a filter of class '{$this->GetClass()}' with a filter of class '{$oFilter->GetClass()}'");
1030		}
1031
1032		// Translate search condition into our aliasing scheme
1033		$aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias();
1034
1035		foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
1036		{
1037			foreach($aPointingTo as $iOperatorCode => $aFilter)
1038			{
1039				foreach($aFilter as $oExtFilter)
1040				{
1041					$this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode);
1042				}
1043			}
1044		}
1045		foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences)
1046		{
1047			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
1048			{
1049				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
1050				{
1051					foreach ($aFilters as $oForeignFilter)
1052					{
1053						$this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode);
1054					}
1055				}
1056			}
1057		}
1058	}
1059
1060	public function GetCriteria() {return $this->m_oSearchCondition;}
1061	public function GetCriteria_FullText() {throw new Exception("Removed GetCriteria_FullText");}
1062	public function GetCriteria_PointingTo($sKeyAttCode = "")
1063	{
1064		if (empty($sKeyAttCode))
1065		{
1066			return $this->m_aPointingTo;
1067		}
1068		if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array();
1069		return $this->m_aPointingTo[$sKeyAttCode];
1070	}
1071	protected function GetCriteria_ReferencedBy()
1072	{
1073		return $this->m_aReferencedBy;
1074	}
1075
1076	public function SetInternalParams($aParams)
1077	{
1078		return $this->m_aParams = $aParams;
1079	}
1080
1081	/**
1082	 * @return array <strong>warning</strong> : array returned by value
1083	 * @see self::GetInternalParamsByRef to get the attribute by reference
1084	 */
1085	public function GetInternalParams()
1086	{
1087		return $this->m_aParams;
1088	}
1089
1090	/**
1091	 * @return array
1092	 * @see http://php.net/manual/en/language.references.return.php
1093	 * @since 2.5.1 N°1582
1094	 */
1095	public function &GetInternalParamsByRef()
1096	{
1097		return $this->m_aParams;
1098	}
1099
1100	/**
1101	 * @param string $sKey
1102	 * @param mixed $value
1103	 * @param bool $bDoNotOverride
1104	 *
1105	 * @throws \CoreUnexpectedValue if $bDoNotOverride and $sKey already exists
1106	 */
1107	public function AddInternalParam($sKey, $value, $bDoNotOverride = false)
1108	{
1109		if ($bDoNotOverride)
1110		{
1111			if (array_key_exists($sKey, $this->m_aParams))
1112			{
1113				throw new CoreUnexpectedValue("The key $sKey already exists with value : ".$this->m_aParams[$sKey]);
1114			}
1115		}
1116
1117		$this->m_aParams[$sKey] = $value;
1118	}
1119
1120	public function GetQueryParams($bExcludeMagicParams = true)
1121	{
1122		$aParams = array();
1123		$this->m_oSearchCondition->Render($aParams, true);
1124
1125		if ($bExcludeMagicParams)
1126		{
1127			$aRet = array();
1128
1129			// Make the list of acceptable arguments... could be factorized with run_query, into oSearch->GetQueryParams($bExclude magic params)
1130			$aNakedMagicArguments = array();
1131			foreach (MetaModel::PrepareQueryArguments(array()) as $sArgName => $value)
1132			{
1133				$iPos = strpos($sArgName, '->object()');
1134				if ($iPos === false)
1135				{
1136					$aNakedMagicArguments[$sArgName] = $value;
1137				}
1138				else
1139				{
1140					$aNakedMagicArguments[substr($sArgName, 0, $iPos)] = true;
1141				}
1142			}
1143			foreach ($aParams as $sParam => $foo)
1144			{
1145				$iPos = strpos($sParam, '->');
1146				if ($iPos === false)
1147				{
1148					$sRefName = $sParam;
1149				}
1150				else
1151				{
1152					$sRefName = substr($sParam, 0, $iPos);
1153				}
1154				if (!array_key_exists($sRefName, $aNakedMagicArguments))
1155				{
1156					$aRet[$sParam] = $foo;
1157				}
1158			}
1159		}
1160
1161		return $aRet;
1162	}
1163
1164	public function ListConstantFields()
1165	{
1166		return $this->m_oSearchCondition->ListConstantFields();
1167	}
1168
1169	/**
1170	 * Turn the parameters (:xxx) into scalar values in order to easily
1171	 * serialize a search
1172	 * @param $aArgs
1173*/
1174	public function ApplyParameters($aArgs)
1175	{
1176		$this->m_oSearchCondition->ApplyParameters(array_merge($this->m_aParams, $aArgs));
1177	}
1178
1179	public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false)
1180	{
1181		// Currently unused, but could be useful later
1182		$bRetrofitParams = false;
1183
1184		if ($bDevelopParams)
1185		{
1186			if (is_null($aContextParams))
1187			{
1188				$aParams = array_merge($this->m_aParams);
1189			}
1190			else
1191			{
1192				$aParams = array_merge($aContextParams, $this->m_aParams);
1193			}
1194			$aParams = MetaModel::PrepareQueryArguments($aParams);
1195		}
1196		else
1197		{
1198			// Leave it as is, the rendering will be made with parameters in clear
1199			$aParams = null;
1200		}
1201
1202		$aSelectedAliases = array();
1203		foreach ($this->m_aSelectedClasses as $sAlias => $sClass)
1204		{
1205			$aSelectedAliases[] = '`' . $sAlias . '`';
1206		}
1207		$sSelectedClasses = implode(', ', $aSelectedAliases);
1208		$sRes = 'SELECT '.$sSelectedClasses.' FROM';
1209
1210		$sRes .= ' ' . $this->GetFirstJoinedClass() . ' AS `' . $this->GetFirstJoinedClassAlias() . '`';
1211		$sRes .= $this->ToOQL_Joins();
1212		$sRes .= " WHERE ".$this->m_oSearchCondition->Render($aParams, $bRetrofitParams);
1213
1214		if ($bWithAllowAllFlag && $this->m_bAllowAllData)
1215		{
1216			$sRes .= " ALLOW ALL DATA";
1217		}
1218		return $sRes;
1219	}
1220
1221	protected function OperatorCodeToOQL($iOperatorCode)
1222	{
1223		switch($iOperatorCode)
1224		{
1225			case TREE_OPERATOR_EQUALS:
1226				$sOperator = ' = ';
1227				break;
1228
1229			case TREE_OPERATOR_BELOW:
1230				$sOperator = ' BELOW ';
1231				break;
1232
1233			case TREE_OPERATOR_BELOW_STRICT:
1234				$sOperator = ' BELOW STRICT ';
1235				break;
1236
1237			case TREE_OPERATOR_NOT_BELOW:
1238				$sOperator = ' NOT BELOW ';
1239				break;
1240
1241			case TREE_OPERATOR_NOT_BELOW_STRICT:
1242				$sOperator = ' NOT BELOW STRICT ';
1243				break;
1244
1245			case TREE_OPERATOR_ABOVE:
1246				$sOperator = ' ABOVE ';
1247				break;
1248
1249			case TREE_OPERATOR_ABOVE_STRICT:
1250				$sOperator = ' ABOVE STRICT ';
1251				break;
1252
1253			case TREE_OPERATOR_NOT_ABOVE:
1254				$sOperator = ' NOT ABOVE ';
1255				break;
1256
1257			case TREE_OPERATOR_NOT_ABOVE_STRICT:
1258				$sOperator = ' NOT ABOVE STRICT ';
1259				break;
1260
1261		}
1262		return $sOperator;
1263	}
1264
1265	protected function ToOQL_Joins()
1266	{
1267		$sRes = '';
1268		foreach($this->m_aPointingTo as $sExtKey => $aPointingTo)
1269		{
1270			foreach($aPointingTo as $iOperatorCode => $aFilter)
1271			{
1272				$sOperator = $this->OperatorCodeToOQL($iOperatorCode);
1273				foreach($aFilter as $oFilter)
1274				{
1275					$sRes .= ' JOIN ' . $oFilter->GetFirstJoinedClass() . ' AS `' . $oFilter->GetFirstJoinedClassAlias() . '` ON `' . $this->GetFirstJoinedClassAlias() . '`.' . $sExtKey . $sOperator . '`' . $oFilter->GetFirstJoinedClassAlias() . '`.id';
1276					$sRes .= $oFilter->ToOQL_Joins();
1277				}
1278			}
1279		}
1280		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
1281		{
1282			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
1283			{
1284				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
1285				{
1286					$sOperator = $this->OperatorCodeToOQL($iOperatorCode);
1287					foreach ($aFilters as $oForeignFilter)
1288					{
1289						$sRes .= ' JOIN ' . $oForeignFilter->GetFirstJoinedClass() . ' AS `' . $oForeignFilter->GetFirstJoinedClassAlias() . '` ON `' . $oForeignFilter->GetFirstJoinedClassAlias() . '`.' . $sForeignExtKeyAttCode . $sOperator . '`' . $this->GetFirstJoinedClassAlias() . '`.id';
1290						$sRes .= $oForeignFilter->ToOQL_Joins();
1291					}
1292				}
1293			}
1294		}
1295		return $sRes;
1296	}
1297
1298	protected function OQLExpressionToCondition($sQuery, $oExpression, $aClassAliases)
1299	{
1300		if ($oExpression instanceof BinaryOqlExpression)
1301		{
1302			$sOperator = $oExpression->GetOperator();
1303			$oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases);
1304			$oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases);
1305			return new BinaryExpression($oLeft, $sOperator, $oRight);
1306		}
1307		elseif ($oExpression instanceof MatchOqlExpression)
1308		{
1309			$oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases);
1310			$oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases);
1311
1312			return new MatchExpression($oLeft, $oRight);
1313		}
1314		elseif ($oExpression instanceof FieldOqlExpression)
1315		{
1316			$sClassAlias = $oExpression->GetParent();
1317			$sFltCode = $oExpression->GetName();
1318			if (empty($sClassAlias))
1319			{
1320				// Need to find the right alias
1321				// Build an array of field => array of aliases
1322				$aFieldClasses = array();
1323				foreach($aClassAliases as $sAlias => $sReal)
1324				{
1325					foreach(MetaModel::GetFiltersList($sReal) as $sAnFltCode)
1326					{
1327						$aFieldClasses[$sAnFltCode][] = $sAlias;
1328					}
1329				}
1330				$sClassAlias = $aFieldClasses[$sFltCode][0];
1331			}
1332			return new FieldExpression($sFltCode, $sClassAlias);
1333		}
1334		elseif ($oExpression instanceof VariableOqlExpression)
1335		{
1336			return new VariableExpression($oExpression->GetName());
1337		}
1338		elseif ($oExpression instanceof TrueOqlExpression)
1339		{
1340			return new TrueExpression;
1341		}
1342		elseif ($oExpression instanceof ScalarOqlExpression)
1343		{
1344			return new ScalarExpression($oExpression->GetValue());
1345		}
1346		elseif ($oExpression instanceof ListOqlExpression)
1347		{
1348			$aItems = array();
1349			foreach ($oExpression->GetItems() as $oItemExpression)
1350			{
1351				$aItems[] = $this->OQLExpressionToCondition($sQuery, $oItemExpression, $aClassAliases);
1352			}
1353			return new ListExpression($aItems);
1354		}
1355		elseif ($oExpression instanceof FunctionOqlExpression)
1356		{
1357			$aArgs = array();
1358			foreach ($oExpression->GetArgs() as $oArgExpression)
1359			{
1360				$aArgs[] = $this->OQLExpressionToCondition($sQuery, $oArgExpression, $aClassAliases);
1361			}
1362			return new FunctionExpression($oExpression->GetVerb(), $aArgs);
1363		}
1364		elseif ($oExpression instanceof IntervalOqlExpression)
1365		{
1366			return new IntervalExpression($oExpression->GetValue(), $oExpression->GetUnit());
1367		}
1368		else
1369		{
1370			throw new CoreException('Unknown expression type', array('class'=>get_class($oExpression), 'query'=>$sQuery));
1371		}
1372	}
1373
1374	public function InitFromOqlQuery(OqlQuery $oOqlQuery, $sQuery)
1375	{
1376		$oModelReflection = new ModelReflectionRuntime();
1377		$sClass = $oOqlQuery->GetClass($oModelReflection);
1378		$sClassAlias = $oOqlQuery->GetClassAlias();
1379
1380		$aAliases = array($sClassAlias => $sClass);
1381
1382		// Note: the condition must be built here, it may be altered later on when optimizing some joins
1383		$oConditionTree = $oOqlQuery->GetCondition();
1384		if ($oConditionTree instanceof Expression)
1385		{
1386			$aRawAliases = array($sClassAlias => $sClass);
1387			$aJoinSpecs = $oOqlQuery->GetJoins();
1388			if (is_array($aJoinSpecs))
1389			{
1390				foreach ($aJoinSpecs as $oJoinSpec)
1391				{
1392					$aRawAliases[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass();
1393				}
1394			}
1395			$this->m_oSearchCondition = $this->OQLExpressionToCondition($sQuery, $oConditionTree, $aRawAliases);
1396		}
1397
1398		// Maintain an array of filters, because the flat list is in fact referring to a tree
1399		// And this will be an easy way to dispatch the conditions
1400		// $this will be referenced by the other filters, or the other way around...
1401		$aJoinItems = array($sClassAlias => $this);
1402
1403		$aJoinSpecs = $oOqlQuery->GetJoins();
1404		if (is_array($aJoinSpecs))
1405		{
1406			$aAliasTranslation = array();
1407			foreach ($aJoinSpecs as $oJoinSpec)
1408			{
1409				$sJoinClass = $oJoinSpec->GetClass();
1410				$sJoinClassAlias = $oJoinSpec->GetClassAlias();
1411				if (isset($aAliasTranslation[$sJoinClassAlias]['*']))
1412				{
1413					$sJoinClassAlias = $aAliasTranslation[$sJoinClassAlias]['*'];
1414				}
1415
1416				// Assumption: ext key on the left only !!!
1417				// normalization should take care of this
1418				$oLeftField = $oJoinSpec->GetLeftField();
1419				$sFromClass = $oLeftField->GetParent();
1420				if (isset($aAliasTranslation[$sFromClass]['*']))
1421				{
1422					$sFromClass = $aAliasTranslation[$sFromClass]['*'];
1423				}
1424				$sExtKeyAttCode = $oLeftField->GetName();
1425
1426				$oRightField = $oJoinSpec->GetRightField();
1427				$sToClass = $oRightField->GetParent();
1428				if (isset($aAliasTranslation[$sToClass]['*']))
1429				{
1430					$sToClass = $aAliasTranslation[$sToClass]['*'];
1431				}
1432
1433				$aAliases[$sJoinClassAlias] = $sJoinClass;
1434				$aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias);
1435
1436				$sOperator = $oJoinSpec->GetOperator();
1437				switch($sOperator)
1438				{
1439					case '=':
1440					default:
1441						$iOperatorCode = TREE_OPERATOR_EQUALS;
1442						break;
1443					case 'BELOW':
1444						$iOperatorCode = TREE_OPERATOR_BELOW;
1445						break;
1446					case 'BELOW_STRICT':
1447						$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
1448						break;
1449					case 'NOT_BELOW':
1450						$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
1451						break;
1452					case 'NOT_BELOW_STRICT':
1453						$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
1454						break;
1455					case 'ABOVE':
1456						$iOperatorCode = TREE_OPERATOR_ABOVE;
1457						break;
1458					case 'ABOVE_STRICT':
1459						$iOperatorCode = TREE_OPERATOR_ABOVE_STRICT;
1460						break;
1461					case 'NOT_ABOVE':
1462						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE;
1463						break;
1464					case 'NOT_ABOVE_STRICT':
1465						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
1466						break;
1467				}
1468
1469				if ($sFromClass == $sJoinClassAlias)
1470				{
1471					$oReceiver = $aJoinItems[$sToClass];
1472					$oNewComer = $aJoinItems[$sFromClass];
1473					$oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode);
1474				}
1475				else
1476				{
1477					$oReceiver = $aJoinItems[$sFromClass];
1478					$oNewComer = $aJoinItems[$sToClass];
1479					$oReceiver->AddCondition_PointingTo_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode);
1480				}
1481			}
1482			$this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false /* leave unresolved fields */);
1483		}
1484
1485		// Check and prepare the select information
1486		$this->m_aSelectedClasses = array();
1487		foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails)
1488		{
1489			$sClassToSelect = $oClassDetails->GetValue();
1490			$this->m_aSelectedClasses[$sClassToSelect] = $aAliases[$sClassToSelect];
1491		}
1492		$this->m_aClasses = $aAliases;
1493	}
1494
1495	////////////////////////////////////////////////////////////////////////////
1496	//
1497	// Construction of the SQL queries
1498	//
1499	////////////////////////////////////////////////////////////////////////////
1500
1501	public function MakeDeleteQuery($aArgs = array())
1502	{
1503		$aModifierProperties = MetaModel::MakeModifierProperties($this);
1504		$oBuild = new QueryBuilderContext($this, $aModifierProperties);
1505		$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => array()), array());
1506		$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
1507		$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
1508		$oSQLQuery->OptimizeJoins(array());
1509		$aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams());
1510		$sRet = $oSQLQuery->RenderDelete($aScalarArgs);
1511		return $sRet;
1512	}
1513
1514	public function MakeUpdateQuery($aValues, $aArgs = array())
1515	{
1516		// $aValues is an array of $sAttCode => $value
1517		$aModifierProperties = MetaModel::MakeModifierProperties($this);
1518		$oBuild = new QueryBuilderContext($this, $aModifierProperties);
1519		$aRequested = array(); // Requested attributes are the updated attributes
1520		foreach ($aValues as $sAttCode => $value)
1521		{
1522			$aRequested[$sAttCode] = MetaModel::GetAttributeDef($this->GetClass(), $sAttCode);
1523		}
1524		$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => $aRequested), $aValues);
1525		$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
1526		$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
1527		$oSQLQuery->OptimizeJoins(array());
1528		$aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams());
1529		$sRet = $oSQLQuery->RenderUpdate($aScalarArgs);
1530		return $sRet;
1531	}
1532
1533	public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
1534	{
1535		// Hide objects that are not visible to the current user
1536		//
1537		$oSearch = $this;
1538		if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered())
1539		{
1540			$oVisibleObjects = UserRights::GetSelectFilter($this->GetClass(), $this->GetModifierProperties('UserRightsGetSelectFilter'));
1541			if ($oVisibleObjects === false)
1542			{
1543				// Make sure this is a valid search object, saying NO for all
1544				$oVisibleObjects = DBObjectSearch::FromEmptySet($this->GetClass());
1545			}
1546			if (is_object($oVisibleObjects))
1547			{
1548				$oVisibleObjects->AllowAllData();
1549				$oSearch = $this->Intersect($oVisibleObjects);
1550				$oSearch->SetDataFiltered();
1551			}
1552		}
1553
1554		// Compute query modifiers properties (can be set in the search itself, by the context, etc.)
1555		//
1556		$aModifierProperties = MetaModel::MakeModifierProperties($oSearch);
1557
1558		// Create a unique cache id
1559		//
1560		$aContextData = array();
1561		$bCanCache = true;
1562		if (self::$m_bQueryCacheEnabled || self::$m_bTraceQueries)
1563		{
1564			if (isset($_SERVER['REQUEST_URI']))
1565			{
1566				$aContextData['sRequestUri'] = $_SERVER['REQUEST_URI'];
1567			}
1568			else if (isset($_SERVER['SCRIPT_NAME']))
1569			{
1570				$aContextData['sRequestUri'] = $_SERVER['SCRIPT_NAME'];
1571			}
1572			else
1573			{
1574				$aContextData['sRequestUri'] = '';
1575			}
1576
1577			// Need to identify the query
1578			$sOqlQuery = $oSearch->ToOql(false, null, true);
1579			if ((strpos($sOqlQuery, '`id` IN (') !== false) || (strpos($sOqlQuery, '`id` NOT IN (') !== false))
1580			{
1581				// Requests containing "id IN" are not worth caching
1582				$bCanCache = false;
1583			}
1584
1585			$aContextData['sOqlQuery'] = $sOqlQuery;
1586
1587			if (count($aModifierProperties))
1588			{
1589				array_multisort($aModifierProperties);
1590				$sModifierProperties = json_encode($aModifierProperties);
1591			}
1592			else
1593			{
1594				$sModifierProperties = '';
1595			}
1596			$aContextData['sModifierProperties'] = $sModifierProperties;
1597
1598			$sRawId = $sOqlQuery.$sModifierProperties;
1599			if (!is_null($aAttToLoad))
1600			{
1601				$sRawId .= json_encode($aAttToLoad);
1602			}
1603			$aContextData['aAttToLoad'] = $aAttToLoad;
1604			if (!is_null($aGroupByExpr))
1605			{
1606				foreach($aGroupByExpr as $sAlias => $oExpr)
1607				{
1608					$sRawId .= 'g:'.$sAlias.'!'.$oExpr->Render();
1609				}
1610			}
1611			if (!is_null($aSelectExpr))
1612			{
1613				foreach($aSelectExpr as $sAlias => $oExpr)
1614				{
1615					$sRawId .= 'se:'.$sAlias.'!'.$oExpr->Render();
1616				}
1617			}
1618			$aContextData['aGroupByExpr'] = $aGroupByExpr;
1619			$aContextData['aSelectExpr'] = $aSelectExpr;
1620			$sRawId .= $bGetCount;
1621			$aContextData['bGetCount'] = $bGetCount;
1622			if (is_array($aSelectedClasses))
1623			{
1624				$sRawId .= implode(',', $aSelectedClasses); // Unions may alter the list of selected columns
1625			}
1626			$aContextData['aSelectedClasses'] = $aSelectedClasses;
1627			$bIsArchiveMode = $oSearch->GetArchiveMode();
1628			$sRawId .= $bIsArchiveMode ? '--arch' : '';
1629			$bShowObsoleteData = $oSearch->GetShowObsoleteData();
1630			$sRawId .= $bShowObsoleteData ? '--obso' : '';
1631			$aContextData['bIsArchiveMode'] = $bIsArchiveMode;
1632			$aContextData['bShowObsoleteData'] = $bShowObsoleteData;
1633			$sOqlId = md5($sRawId);
1634		}
1635		else
1636		{
1637			$sOqlQuery = "SELECTING... ".$oSearch->GetClass();
1638			$sOqlId = "query id ? n/a";
1639		}
1640
1641
1642		// Query caching
1643		//
1644		$sOqlAPCCacheId = null;
1645		if (self::$m_bQueryCacheEnabled)
1646		{
1647			// Warning: using directly the query string as the key to the hash array can FAIL if the string
1648			// is long and the differences are only near the end... so it's safer (but not bullet proof?)
1649			// to use a hash (like md5) of the string as the key !
1650			//
1651			// Example of two queries that were found as similar by the hash array:
1652			// SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTO' AND CustomerContract.customer_id = 2
1653			// and
1654			// SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTR' AND CustomerContract.customer_id = 2
1655			// the only difference is R instead or O at position 285 (TTR instead of TTO)...
1656			//
1657			if (array_key_exists($sOqlId, self::$m_aQueryStructCache))
1658			{
1659				// hit!
1660
1661				$oSQLQuery = unserialize(serialize(self::$m_aQueryStructCache[$sOqlId]));
1662				// Note: cloning is not enough because the subtree is made of objects
1663			}
1664			elseif (self::$m_bUseAPCCache)
1665			{
1666				// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
1667				//
1668				$sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-query-cache-'.$sOqlId;
1669				$oKPI = new ExecutionKPI();
1670				$result = apc_fetch($sOqlAPCCacheId);
1671				$oKPI->ComputeStats('Query APC (fetch)', $sOqlQuery);
1672
1673				if (is_object($result))
1674				{
1675					$oSQLQuery = $result;
1676					self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery;
1677				}
1678			}
1679		}
1680
1681		if (!isset($oSQLQuery))
1682		{
1683			$oKPI = new ExecutionKPI();
1684			$oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
1685			$oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery);
1686
1687			if (self::$m_bQueryCacheEnabled)
1688			{
1689				if ($bCanCache && self::$m_bUseAPCCache)
1690				{
1691					$oSQLQuery->m_aContextData = $aContextData;
1692					$oKPI = new ExecutionKPI();
1693					apc_store($sOqlAPCCacheId, $oSQLQuery, self::$m_iQueryCacheTTL);
1694					$oKPI->ComputeStats('Query APC (store)', $sOqlQuery);
1695				}
1696
1697				self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery->DeepClone();
1698			}
1699		}
1700		return $oSQLQuery;
1701	}
1702
1703	/**
1704	 * @param array $aAttToLoad
1705	 * @param bool $bGetCount
1706	 * @param array $aModifierProperties
1707	 * @param array $aGroupByExpr
1708	 * @param array $aSelectedClasses
1709	 * @param array $aSelectExpr
1710	 *
1711	 * @return null|SQLObjectQuery
1712	 * @throws \CoreException
1713	 */
1714	protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
1715	{
1716		$oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
1717
1718		$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array());
1719		$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
1720		if (is_array($aGroupByExpr))
1721		{
1722			$aCols = $oBuild->m_oQBExpressions->GetGroupBy();
1723			$oSQLQuery->SetGroupBy($aCols);
1724			$oSQLQuery->SetSelect($aCols);
1725		}
1726		else
1727		{
1728			$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
1729		}
1730		if ($aSelectExpr)
1731		{
1732			// Get the fields corresponding to the select expressions
1733			foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr)
1734			{
1735				if (key_exists($sAlias, $aSelectExpr))
1736				{
1737					$oSQLQuery->AddSelect($sAlias, $oExpr);
1738				}
1739			}
1740		}
1741
1742		$aMandatoryTables = null;
1743		if (self::$m_bOptimizeQueries)
1744		{
1745			if ($bGetCount)
1746			{
1747				// Simplify the query if just getting the count
1748				$oSQLQuery->SetSelect(array());
1749			}
1750			$oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables);
1751			$oSQLQuery->OptimizeJoins($aMandatoryTables);
1752		}
1753		// Filter tables as late as possible: do not interfere with the optimization process
1754		foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions)
1755		{
1756			if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables))
1757			{
1758				foreach ($aConditions as $oCondition)
1759				{
1760					$oSQLQuery->AddCondition($oCondition);
1761				}
1762			}
1763		}
1764
1765		return $oSQLQuery;
1766	}
1767
1768
1769	/**
1770	 * @param $oBuild
1771	 * @param null $aAttToLoad
1772	 * @param array $aValues
1773	 * @return null|SQLObjectQuery
1774	 * @throws \CoreException
1775	 */
1776	protected function MakeSQLObjectQuery(&$oBuild, $aAttToLoad = null, $aValues = array())
1777	{
1778		// Note: query class might be different than the class of the filter
1779		// -> this occurs when we are linking our class to an external class (referenced by, or pointing to)
1780		$sClass = $this->GetFirstJoinedClass();
1781		$sClassAlias = $this->GetFirstJoinedClassAlias();
1782
1783		$bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
1784
1785		//self::DbgTrace("Entering: ".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
1786
1787		//$sRootClass = MetaModel::GetRootClass($sClass);
1788		$sKeyField = MetaModel::DBGetKey($sClass);
1789
1790		if ($bIsOnQueriedClass)
1791		{
1792			// default to the whole list of attributes + the very std id/finalclass
1793			$oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias));
1794			if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad))
1795			{
1796				$sSelectedClass = $oBuild->GetSelectedClass($sClassAlias);
1797				$aAttList = MetaModel::ListAttributeDefs($sSelectedClass);
1798			}
1799			else
1800			{
1801				$aAttList = $aAttToLoad[$sClassAlias];
1802			}
1803			foreach ($aAttList as $sAttCode => $oAttDef)
1804			{
1805				if (!$oAttDef->IsScalar()) continue;
1806				// keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue;
1807
1808				if ($oAttDef->IsBasedOnOQLExpression())
1809				{
1810					$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode, new FieldExpression($sAttCode, $sClassAlias));
1811				}
1812				else
1813				{
1814					foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
1815					{
1816						$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias));
1817					}
1818				}
1819			}
1820		}
1821		//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
1822		$aExpectedAtts = array(); // array of (attcode => fieldexpression)
1823		//echo "<p>".__LINE__.": GetUnresolvedFields($sClassAlias, ...)</p>\n";
1824		$oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts);
1825
1826		// Compute a clear view of required joins (from the current class)
1827		// Build the list of external keys:
1828		// -> ext keys required by an explicit join
1829		// -> ext keys mentionned in a 'pointing to' condition
1830		// -> ext keys required for an external field
1831		// -> ext keys required for a friendly name
1832		//
1833		$aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef))
1834		//
1835		// Optimization: could be partially computed once for all (cached) ?
1836		//
1837
1838		if ($bIsOnQueriedClass)
1839		{
1840			// Get all Ext keys for the queried class (??)
1841			foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode)
1842			{
1843				$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
1844				$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
1845			}
1846		}
1847		// Get all Ext keys used by the filter
1848		foreach ($this->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo)
1849		{
1850			if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
1851			{
1852				$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
1853				$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
1854			}
1855		}
1856
1857		$aFNJoinAlias = array(); // array of (subclass => alias)
1858		foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression)
1859		{
1860			if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue;
1861			$oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode);
1862			if ($oAttDef->IsBasedOnOQLExpression())
1863			{
1864				// To optimize: detect a restriction on child classes in the condition expression
1865				//    e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine')
1866				$oExpression = static::GetPolymorphicExpression($sClass, $sExpectedAttCode);
1867
1868				$aRequiredFields = array();
1869				$oExpression->GetUnresolvedFields('', $aRequiredFields);
1870				$aTranslateFields = array();
1871				foreach($aRequiredFields as $sSubClass => $aFields)
1872				{
1873					foreach($aFields as $sAttCode => $oField)
1874					{
1875						$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
1876						if ($oAttDef->IsExternalKey())
1877						{
1878							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
1879							$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
1880						}
1881						elseif ($oAttDef->IsExternalField())
1882						{
1883							$sKeyAttCode = $oAttDef->GetKeyAttCode();
1884							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
1885							$aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef;
1886						}
1887						else
1888						{
1889							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
1890						}
1891
1892						if (MetaModel::IsParentClass($sClassOfAttribute, $sClass))
1893						{
1894							// The attribute is part of the standard query
1895							//
1896							$sAliasForAttribute = $sClassAlias;
1897						}
1898						else
1899						{
1900							// The attribute will be available from an additional outer join
1901							// For each subclass (table) one single join is enough
1902							//
1903							if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias))
1904							{
1905								$sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute);
1906								$aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute;
1907							}
1908							else
1909							{
1910								$sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute];
1911							}
1912						}
1913
1914						$aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute);
1915					}
1916				}
1917				$oExpression = $oExpression->Translate($aTranslateFields, false);
1918
1919				$aTranslateNow = array();
1920				$aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression;
1921				$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
1922			}
1923		}
1924
1925		// Add the ext fields used in the select (eventually adds an external key)
1926		foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
1927		{
1928			if ($oAttDef->IsExternalField())
1929			{
1930				if (array_key_exists($sAttCode, $aExpectedAtts))
1931				{
1932					// Add the external attribute
1933					$sKeyAttCode = $oAttDef->GetKeyAttCode();
1934					$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
1935					$aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef;
1936				}
1937			}
1938		}
1939
1940		$bRootFirst = MetaModel::GetConfig()->Get('optimize_requests_for_join_count');
1941		if ($bRootFirst)
1942		{
1943			// First query built from the root, adding all tables including the leaf
1944			//   Before N.1065 we were joining from the leaf first, but this wasn't a good choice :
1945			//   most of the time (obsolescence, friendlyname, ...) we want to get a root attribute !
1946			//
1947			$oSelectBase = null;
1948			$aClassHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, true);
1949			$bIsClassStandaloneClass = (count($aClassHierarchy) == 1);
1950			foreach($aClassHierarchy as $sSomeClass)
1951			{
1952				if (!MetaModel::HasTable($sSomeClass))
1953				{
1954					continue;
1955				}
1956
1957				self::DbgTrace("Adding join from root to leaf: $sSomeClass... let's call MakeSQLObjectQuerySingleTable()");
1958				$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSomeClass, $aExtKeys, $aValues);
1959				if (is_null($oSelectBase))
1960				{
1961					$oSelectBase = $oSelectParentTable;
1962					if (!$bIsClassStandaloneClass && (MetaModel::IsRootClass($sSomeClass)))
1963					{
1964						// As we're linking to root class first, we're adding a where clause on the finalClass attribute :
1965						//      COALESCE($sRootClassFinalClass IN ('$sExpectedClasses'), 1)
1966						// If we don't, the child classes can be removed in the query optimisation phase, including the leaf that was queried
1967						// So we still need to filter records to only those corresponding to the child classes !
1968						// The coalesce is mandatory if we have a polymorphic query (left join)
1969						$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
1970						$sFinalClassSqlColumnName = MetaModel::DBGetClassField($sSomeClass);
1971						$oClassExpr = new FieldExpression($sFinalClassSqlColumnName, $oSelectBase->GetTableAlias());
1972						$oInExpression = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
1973						$oTrueExpression = new TrueExpression();
1974						$aCoalesceAttr = array($oInExpression, $oTrueExpression);
1975						$oFinalClassRestriction = new FunctionExpression("COALESCE", $aCoalesceAttr);
1976
1977						$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
1978					}
1979				}
1980				else
1981				{
1982					$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass));
1983				}
1984			}
1985		}
1986		else
1987		{
1988			// First query built upon on the leaf (ie current) class
1989			//
1990			self::DbgTrace("Main (=leaf) class, call MakeSQLObjectQuerySingleTable()");
1991			if (MetaModel::HasTable($sClass))
1992			{
1993				$oSelectBase = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sClass, $aExtKeys, $aValues);
1994			}
1995			else
1996			{
1997				$oSelectBase = null;
1998
1999				// As the join will not filter on the expected classes, we have to specify it explicitely
2000				$sExpectedClasses = implode("', '", MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
2001				$oFinalClassRestriction = Expression::FromOQL("`$sClassAlias`.finalclass IN ('$sExpectedClasses')");
2002				$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
2003			}
2004
2005			// Then we join the queries of the eventual parent classes (compound model)
2006			foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
2007			{
2008				if (!MetaModel::HasTable($sParentClass)) continue;
2009
2010				self::DbgTrace("Parent class: $sParentClass... let's call MakeSQLObjectQuerySingleTable()");
2011				$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sParentClass, $aExtKeys, $aValues);
2012				if (is_null($oSelectBase))
2013				{
2014					$oSelectBase = $oSelectParentTable;
2015				}
2016				else
2017				{
2018					$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sParentClass));
2019				}
2020			}
2021		}
2022
2023		// Filter on objects referencing me
2024		//
2025		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
2026		{
2027			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
2028			{
2029				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
2030				{
2031					foreach ($aFilters as $oForeignFilter)
2032					{
2033						$oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
2034
2035						self::DbgTrace("Referenced by foreign key: $sForeignExtKeyAttCode... let's call MakeSQLObjectQuery()");
2036						//self::DbgTrace($oForeignFilter);
2037						//self::DbgTrace($oForeignFilter->ToOQL());
2038						//self::DbgTrace($oSelectForeign);
2039						//self::DbgTrace($oSelectForeign->RenderSelect(array()));
2040
2041						$sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias();
2042						$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias));
2043
2044						if ($oForeignKeyAttDef instanceof AttributeObjectKey)
2045						{
2046							$sClassAttCode = $oForeignKeyAttDef->Get('class_attcode');
2047
2048							// Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass')
2049							$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
2050							$oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias);
2051							$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
2052							$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
2053						}
2054
2055						$oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
2056
2057						$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
2058						$sForeignKeyTable = $oJoinExpr->GetParent();
2059						$sForeignKeyColumn = $oJoinExpr->GetName();
2060
2061						if ($iOperatorCode == TREE_OPERATOR_EQUALS)
2062						{
2063							$oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable);
2064						}
2065						else
2066						{
2067							// Hierarchical key
2068							$KeyLeft = $oForeignKeyAttDef->GetSQLLeft();
2069							$KeyRight = $oForeignKeyAttDef->GetSQLRight();
2070
2071							$oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true);
2072						}
2073					}
2074				}
2075			}
2076		}
2077
2078		// Additional JOINS for Friendly names
2079		//
2080		foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias)
2081		{
2082			$oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias);
2083			$oSelectFN = $oSubClassFilter->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSubClass, $aExtKeys, array());
2084			$oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass));
2085		}
2086
2087		// That's all... cross fingers and we'll get some working query
2088
2089		//MyHelpers::var_dump_html($oSelectBase, true);
2090		//MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true);
2091		if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml();
2092		return $oSelectBase;
2093	}
2094
2095	protected function MakeSQLObjectQuerySingleTable(&$oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues)
2096	{
2097		// $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields))
2098
2099		// Prepare the query for a single table (compound objects)
2100		// Ignores the items (attributes/filters) that are not on the target table
2101		// Perform an (inner or left) join for every external key (and specify the expected fields)
2102		//
2103		// Returns an SQLQuery
2104		//
2105		$sTargetClass = $this->GetFirstJoinedClass();
2106		$sTargetAlias = $this->GetFirstJoinedClassAlias();
2107		$sTable = MetaModel::DBGetTable($sTableClass);
2108		$sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable);
2109
2110		$aTranslation = array();
2111		$aExpectedAtts = array();
2112		$oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts);
2113
2114		$bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
2115
2116		self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
2117
2118		// 1 - SELECT and UPDATE
2119		//
2120		// Note: no need for any values nor fields for foreign Classes (ie not the queried Class)
2121		//
2122		$aUpdateValues = array();
2123
2124
2125		// 1/a - Get the key and friendly name
2126		//
2127		// We need one pkey to be the key, let's take the first one available
2128		$oSelectedIdField = null;
2129		$oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias);
2130		$aTranslation[$sTargetAlias]['id'] = $oIdField;
2131
2132		if ($bIsOnQueriedClass)
2133		{
2134			// Add this field to the list of queried fields (required for the COUNT to work fine)
2135			$oSelectedIdField = $oIdField;
2136		}
2137
2138		// 1/b - Get the other attributes
2139		//
2140		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
2141		{
2142			// Skip this attribute if not defined in this table
2143			if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
2144
2145			// Skip this attribute if not made of SQL columns
2146			if (count($oAttDef->GetSQLExpressions()) == 0) continue;
2147
2148			// Update...
2149			//
2150			if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues))
2151			{
2152				assert ($oAttDef->IsBasedOnDBColumns());
2153				foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue)
2154				{
2155					$aUpdateValues[$sColumn] = $sValue;
2156				}
2157			}
2158		}
2159
2160		// 2 - The SQL query, for this table only
2161		//
2162		$oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField);
2163
2164		// 3 - Resolve expected expressions (translation table: alias.attcode => table.column)
2165		//
2166		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
2167		{
2168			// Skip this attribute if not defined in this table
2169			if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
2170
2171			// Select...
2172			//
2173			if ($oAttDef->IsExternalField())
2174			{
2175				// skip, this will be handled in the joined tables (done hereabove)
2176			}
2177			else
2178			{
2179				// standard field, or external key
2180				// add it to the output
2181				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
2182				{
2183					if (array_key_exists($sAttCode.$sColId, $aExpectedAtts))
2184					{
2185						$oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
2186						foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier)
2187						{
2188							$oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase);
2189						}
2190						$aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp;
2191					}
2192				}
2193			}
2194		}
2195
2196		// 4 - The external keys -> joins...
2197		//
2198		$aAllPointingTo = $this->GetCriteria_PointingTo();
2199
2200		if (array_key_exists($sTableClass, $aExtKeys))
2201		{
2202			foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields)
2203			{
2204				$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
2205
2206				$aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode);
2207				if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
2208				{
2209					// The join was not explicitely defined in the filter,
2210					// we need to do it now
2211					$sKeyClass =  $oKeyAttDef->GetTargetClass();
2212					$sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass);
2213					$oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias);
2214
2215					$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter;
2216				}
2217			}
2218		}
2219
2220		foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
2221		{
2222			foreach($aPointingTo as $iOperatorCode => $aFilter)
2223			{
2224				foreach($aFilter as $oExtFilter)
2225				{
2226					if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it
2227					// The aliases should not conflict because normalization occured while building the filter
2228					$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
2229					$sKeyClass =  $oExtFilter->GetFirstJoinedClass();
2230					$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
2231
2232					// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree
2233
2234					if ($iOperatorCode == TREE_OPERATOR_EQUALS)
2235					{
2236						if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
2237						{
2238							// Specify expected attributes for the target class query
2239							// ... and use the current alias !
2240							$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
2241							foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
2242							{
2243								$oExtAttDef = $oAtt->GetExtAttDef();
2244								if ($oExtAttDef->IsBasedOnOQLExpression())
2245								{
2246									$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression($oExtAttDef->GetCode(), $sKeyClassAlias);
2247								}
2248								else
2249								{
2250									$sExtAttCode = $oAtt->GetExtAttCode();
2251									// Translate mainclass.extfield => remoteclassalias.remotefieldcode
2252									$oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode);
2253									foreach ($oRemoteAttDef->GetSQLExpressions() as $sColId => $sRemoteAttExpr)
2254									{
2255										$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
2256									}
2257								}
2258							}
2259
2260							if ($oKeyAttDef instanceof AttributeObjectKey)
2261							{
2262								// Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass')
2263								$sClassAttCode = $oKeyAttDef->Get('class_attcode');
2264								$oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode);
2265								foreach ($oClassAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
2266								{
2267									$aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
2268								}
2269
2270								$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL));
2271								$oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias);
2272								$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
2273								$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
2274							}
2275
2276							// Translate prior to recursing
2277							//
2278							$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
2279
2280							self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()");
2281							$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
2282
2283							$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
2284
2285							$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
2286							$sExternalKeyTable = $oJoinExpr->GetParent();
2287							$sExternalKeyField = $oJoinExpr->GetName();
2288
2289							$aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
2290							$sLocalKeyField = current($aCols); // get the first column for an external key
2291
2292							self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
2293							if ($oKeyAttDef->IsNullAllowed())
2294							{
2295								$oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField);
2296							}
2297							else
2298							{
2299								$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
2300							}
2301						}
2302					}
2303					elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass)
2304					{
2305						$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
2306						$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
2307						$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
2308						$sExternalKeyTable = $oJoinExpr->GetParent();
2309						$sExternalKeyField = $oJoinExpr->GetName();
2310						$sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft()
2311						$sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight()
2312
2313						$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
2314						$LocalKeyRight = $oKeyAttDef->GetSQLRight();
2315
2316						$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
2317					}
2318				}
2319			}
2320		}
2321
2322		// Translate the selected columns
2323		//
2324		$oBuild->m_oQBExpressions->Translate($aTranslation, false);
2325
2326		// Filter out archived records
2327		//
2328		if (MetaModel::IsArchivable($sTableClass))
2329		{
2330			if (!$oBuild->GetRootFilter()->GetArchiveMode())
2331			{
2332				$bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses());
2333				if ($bIsOnJoinedClass)
2334				{
2335					if (MetaModel::IsParentClass($sTableClass, $sTargetClass))
2336					{
2337						$oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0));
2338						$oBuild->AddFilteredTable($sTableAlias, $oNotArchived);
2339					}
2340				}
2341			}
2342		}
2343		return $oSelectBase;
2344	}
2345
2346	/**
2347	 *    Get the expression for the class and its subclasses (if finalclass = 'subclass' ...)
2348	 *    Simplifies the final expression by grouping classes having the same expression
2349	 * @param $sClass
2350	 * @param $sAttCode
2351	 * @return \FunctionExpression|mixed|null
2352	 * @throws \CoreException
2353*/
2354	static public function GetPolymorphicExpression($sClass, $sAttCode)
2355	{
2356		$oExpression = ExpressionCache::GetCachedExpression($sClass, $sAttCode);
2357		if (!empty($oExpression))
2358		{
2359			return $oExpression;
2360		}
2361
2362		// 1st step - get all of the required expressions (instantiable classes)
2363		//            and group them using their OQL representation
2364		//
2365		$aExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes)
2366		foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass)
2367		{
2368			if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue;
2369
2370			$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
2371			$oSubClassExp = $oAttDef->GetOQLExpression($sSubClass);
2372
2373			// 3rd step - position the attributes in the hierarchy of classes
2374			//
2375			$oSubClassExp->Browse(function($oNode) use ($sSubClass) {
2376				if ($oNode instanceof FieldExpression)
2377				{
2378					$sAttCode = $oNode->GetName();
2379					$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
2380					if ($oAttDef->IsExternalField())
2381					{
2382						$sKeyAttCode = $oAttDef->GetKeyAttCode();
2383						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
2384					}
2385					else
2386					{
2387						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
2388					}
2389					$sParent = MetaModel::GetAttributeOrigin($sClassOfAttribute, $oNode->GetName());
2390					$oNode->SetParent($sParent);
2391				}
2392			});
2393
2394			$sSignature = $oSubClassExp->Render();
2395			if (!array_key_exists($sSignature, $aExpressions))
2396			{
2397				$aExpressions[$sSignature] = array(
2398					'expression' => $oSubClassExp,
2399					'classes' => array(),
2400				);
2401			}
2402			$aExpressions[$sSignature]['classes'][] = $sSubClass;
2403		}
2404
2405		// 2nd step - build the final name expression depending on the finalclass
2406		//
2407		if (count($aExpressions) == 1)
2408		{
2409			$aExpData = reset($aExpressions);
2410			$oExpression = $aExpData['expression'];
2411		}
2412		else
2413		{
2414			$oExpression = null;
2415			foreach ($aExpressions as $sSignature => $aExpData)
2416			{
2417				$oClassListExpr = ListExpression::FromScalars($aExpData['classes']);
2418				$oClassExpr = new FieldExpression('finalclass', $sClass);
2419				$oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
2420
2421				if (is_null($oExpression))
2422				{
2423					$oExpression = $aExpData['expression'];
2424				}
2425				else
2426				{
2427					$oExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oExpression));
2428				}
2429			}
2430		}
2431		return $oExpression;
2432	}
2433}
2434