瀏覽代碼

OQL normalization and dashlets have been made independent from the class MetaModel
Added OQL normalization unit tests (to be run on a standard installation)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2767 a333f486-631f-4898-b8df-5754b55c2be0

romainq 12 年之前
父節點
當前提交
4fe6b12cb0

+ 4 - 3
application/dashboard.class.inc.php

@@ -18,6 +18,7 @@
 
 require_once(APPROOT.'application/dashboardlayout.class.inc.php');
 require_once(APPROOT.'application/dashlet.class.inc.php');
+require_once(APPROOT.'core/modelreflection.class.inc.php');
 
 /**
  * A user editable dashboard page
@@ -82,7 +83,7 @@ abstract class Dashboard
 					$iRank = (float)$oRank->textContent;
 				}
 				$sId = $oDomNode->getAttribute('id');
-				$oNewDashlet = new $sDashletClass($sId);
+				$oNewDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sId);
 				$oNewDashlet->FromDOMNode($oDomNode);
 				$aDashletOrder[] = array('rank' => $iRank, 'dashlet' => $oNewDashlet);
 			}
@@ -183,7 +184,7 @@ abstract class Dashboard
 			{
 				$sDashletClass = $aDashletParams['dashlet_class'];
 				$sId = $aDashletParams['dashlet_id'];
-				$oNewDashlet = new $sDashletClass($sId);
+				$oNewDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sId);
 				
 				$oForm = $oNewDashlet->GetForm();
 				$oForm->SetParamsContainer($sId);
@@ -687,7 +688,7 @@ EOF
 		foreach($aDashlets as $sDashletClass => $aDashletInfo)
 		{
 			$oSubForm = new DesignerForm();
-			$oDashlet = new $sDashletClass(0);
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0);
 			$oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL);
 			
 			$oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']);

+ 38 - 36
application/dashlet.class.inc.php

@@ -26,14 +26,16 @@ require_once(APPROOT.'application/forms.class.inc.php');
  */
 abstract class Dashlet
 {
+	protected $oModelReflection;
 	protected $sId;
 	protected $bRedrawNeeded;
 	protected $bFormRedrawNeeded;
 	protected $aProperties; // array of {property => value}
 	protected $aCSSClasses;
 	
-	public function __construct($sId)
+	public function __construct(ModelReflection $oModelReflection, $sId)
 	{
+		$this->oModelReflection = $oModelReflection;
 		$this->sId = $sId;
 		$this->bRedrawNeeded = true; // By default: redraw each time a property changes
 		$this->bFormRedrawNeeded = false; // By default: no need to redraw the form (independent fields)
@@ -268,9 +270,9 @@ EOF
 
 class DashletEmptyCell extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 	}
 	
 	public function Render($oPage, $bEditMode = false, $aExtraParams = array())
@@ -299,9 +301,9 @@ class DashletEmptyCell extends Dashlet
 
 class DashletPlainText extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['text'] = Dict::S('UI:DashletPlainText:Prop-Text:Default');
 	}
 	
@@ -332,9 +334,9 @@ class DashletPlainText extends Dashlet
 
 class DashletObjectList extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['title'] = '';
 		$this->aProperties['query'] = 'SELECT Contact';
 		$this->aProperties['menu'] = false;
@@ -406,9 +408,9 @@ class DashletObjectList extends Dashlet
 
 abstract class DashletGroupBy extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['title'] = '';
 		$this->aProperties['query'] = 'SELECT Contact';
 		$this->aProperties['group_by'] = 'status';
@@ -439,13 +441,13 @@ abstract class DashletGroupBy extends Dashlet
 			$sAttCode = $sGroupBy;
 			$sFunction = null;
 		}
-		if (!MetaModel::IsValidAttCode($sClass, $sAttCode))
+		if (!$this->oModelReflection->IsValidAttCode($sClass, $sAttCode))
 		{
 			$oPage->add('<p>'.Dict::S('UI:DashletGroupBy:MissingGroupBy').'</p>');
 		}
 		else
 		{
-			$sAttLabel = MetaModel::GetLabel($sClass, $sAttCode);
+			$sAttLabel = $this->oModelReflection->GetLabel($sClass, $sAttCode);
 			if (!is_null($sFunction))
 			{
 				$sFunction = $aMatches[2];
@@ -534,7 +536,7 @@ abstract class DashletGroupBy extends Dashlet
 		$oSearch = DBObjectSearch::FromOQL($sOql);
 		$sClass = $oSearch->GetClass();
 		$aGroupBy = array();
-		foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+		foreach($this->oModelReflection->ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
 		{
 			if (!$oAttDef->IsScalar()) continue; // skip link sets
 			if ($oAttDef instanceof AttributeFriendlyName) continue;
@@ -691,9 +693,9 @@ abstract class DashletGroupBy extends Dashlet
 
 class DashletGroupByPie extends DashletGroupBy
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['style'] = 'pie';
 	}
 	
@@ -710,9 +712,9 @@ class DashletGroupByPie extends DashletGroupBy
 
 class DashletGroupByBars extends DashletGroupBy
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['style'] = 'bars';
 	}
 	
@@ -728,9 +730,9 @@ class DashletGroupByBars extends DashletGroupBy
 
 class DashletGroupByTable extends DashletGroupBy
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['style'] = 'table';
 	}
 	
@@ -747,11 +749,11 @@ class DashletGroupByTable extends DashletGroupBy
 
 class DashletHeaderStatic extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['title'] = Dict::S('UI:DashletHeaderStatic:Prop-Title:Default');
-		$sIcon = MetaModel::GetClassIcon('Contact', false);
+		$sIcon = $this->oModelReflection->GetClassIcon('Contact', false);
 		$sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIcon);
 		$this->aProperties['icon'] = $sIcon;
 	}
@@ -827,11 +829,11 @@ class DashletHeaderStatic extends Dashlet
 
 class DashletHeaderDynamic extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['title'] = Dict::S('UI:DashletHeaderDynamic:Prop-Title:Default');
-		$sIcon = MetaModel::GetClassIcon('Contact', false);
+		$sIcon = $this->oModelReflection->GetClassIcon('Contact', false);
 		$sIcon = str_replace(utils::GetAbsoluteUrlModulesRoot(), '', $sIcon);
 		$this->aProperties['icon'] = $sIcon;
 		$this->aProperties['subtitle'] = Dict::S('UI:DashletHeaderDynamic:Prop-Subtitle:Default');
@@ -854,11 +856,11 @@ class DashletHeaderDynamic extends Dashlet
 
 		$sIconPath = utils::GetAbsoluteUrlModulesRoot().$sIcon;
 
-		if (MetaModel::IsValidAttCode($sClass, $sGroupBy))
+		if ($this->oModelReflection->IsValidAttCode($sClass, $sGroupBy))
 		{
 			if (count($aValues) == 0)
 			{
-				$aAllowed = MetaModel::GetAllowedValues_att($sClass, $sGroupBy);
+				$aAllowed = $this->oModelReflection->GetAllowedValues_att($sClass, $sGroupBy);
 				if (is_array($aAllowed))
 				{
 					$aValues = array_keys($aAllowed);
@@ -929,9 +931,9 @@ class DashletHeaderDynamic extends Dashlet
 			$oSearch = DBObjectSearch::FromOQL($this->aProperties['query']);
 			$sClass = $oSearch->GetClass();
 			$aGroupBy = array();
-			foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+			foreach($this->oModelReflection->ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
 			{
-				if (!$oAttDef instanceof AttributeEnum && (!$oAttDef instanceof AttributeFinalClass || !MetaModel::HasChildrenClasses($sClass))) continue;
+				if (!$oAttDef instanceof AttributeEnum && (!$oAttDef instanceof AttributeFinalClass || !$this->oModelReflection->HasChildrenClasses($sClass))) continue;
 				$sLabel = $oAttDef->GetLabel();
 				$aGroupBy[$sAttCode] = $sLabel;
 			}
@@ -948,9 +950,9 @@ class DashletHeaderDynamic extends Dashlet
 
 		$oField = new DesignerComboField('values', Dict::S('UI:DashletHeaderDynamic:Prop-Values'), $this->aProperties['values']);
 		$oField->MultipleSelection(true);
-		if (isset($sClass) && MetaModel::IsValidAttCode($sClass, $this->aProperties['group_by']))
+		if (isset($sClass) && $this->oModelReflection->IsValidAttCode($sClass, $this->aProperties['group_by']))
 		{
-			$aValues = MetaModel::GetAllowedValues_att($sClass, $this->aProperties['group_by']);
+			$aValues = $this->oModelReflection->GetAllowedValues_att($sClass, $this->aProperties['group_by']);
 			$oField->SetAllowedValues($aValues);
 		}
 		else
@@ -1008,9 +1010,9 @@ class DashletHeaderDynamic extends Dashlet
 
 class DashletBadge extends Dashlet
 {
-	public function __construct($sId)
+	public function __construct($oModelReflection, $sId)
 	{
-		parent::__construct($sId);
+		parent::__construct($oModelReflection, $sId);
 		$this->aProperties['class'] = 'Contact';
 		$this->aCSSClasses[] = 'dashlet-inline';
 		$this->aCSSClasses[] = 'dashlet-badge';
@@ -1046,9 +1048,9 @@ class DashletBadge extends Dashlet
 		
 		$aLinkClasses = array();
 	
-		foreach(MetaModel::GetClasses('bizmodel') as $sClass)
+		foreach($this->oModelReflection->GetClasses('bizmodel') as $sClass)
 		{	
-			foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+			foreach($this->oModelReflection->ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
 			{
 				if ($oAttDef instanceof AttributeLinkedSetIndirect)
 				{
@@ -1065,14 +1067,14 @@ class DashletBadge extends Dashlet
 		{
 			if (!array_key_exists($sClass, $aLinkClasses))
 			{
-				$sIconUrl = MetaModel::GetClassIcon($sClass, false);
+				$sIconUrl = $this->oModelReflection->GetClassIcon($sClass, false);
 				$sIconFilePath = str_replace(utils::GetAbsoluteUrlAppRoot(), APPROOT, $sIconUrl);
 				if (($sIconUrl == '') || !file_exists($sIconFilePath))
 				{
 					// The icon does not exist, leet's use a transparent one of the same size.
 					$sIconUrl = utils::GetAbsoluteUrlAppRoot().'images/transparent_32_32.png';
 				}
-				$aValues[] = array('value' => $sClass, 'label' => MetaModel::GetName($sClass), 'icon' => $sIconUrl);
+				$aValues[] = array('value' => $sClass, 'label' => $this->oModelReflection->GetName($sClass), 'icon' => $sIconUrl);
 			}
 		}
 		$oField->SetAllowedValues($aValues);

+ 39 - 92
core/dbobjectsearch.class.php

@@ -720,6 +720,19 @@ class DBObjectSearch
 
 	public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS)
 	{
+		if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode))
+		{
+			throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}'");
+		}
+		$oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
+		if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass()))
+		{
+			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()}");
+		}
+		if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
+		{
+			throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
+		}
 		// Note: though it seems to be a good practice to clone the given source filter
 		//       (as it was done and fixed an issue in MergeWith())
 		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
@@ -734,20 +747,6 @@ class DBObjectSearch
 
 	protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
 	{
-		if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode))
-		{
-			throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}' - the condition will be ignored");
-		}
-		$oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
-		if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass()))
-		{
-			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()}");
-		}
-		if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
-		{
-			throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
-		}
-
 		// Find the node on which the new tree must be attached (most of the time it is "this")
 		$oReceivingFilter = $this->GetNode($this->GetClassAlias());
 
@@ -757,6 +756,17 @@ class DBObjectSearch
 
 	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode)
 	{
+		$sForeignClass = $oFilter->GetClass();
+		if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode))
+		{
+			throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}'");
+		}
+		$oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
+		if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass()))
+		{
+			// à refaire en spécifique dans FromOQL
+			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()}");
+		}
 		// Note: though it seems to be a good practice to clone the given source filter
 		//       (as it was done and fixed an issue in MergeWith())
 		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
@@ -772,16 +782,6 @@ class DBObjectSearch
 	protected function AddCondition_ReferencedBy_InNameSpace(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation)
 	{
 		$sForeignClass = $oFilter->GetClass();
-		$sForeignClassAlias = $oFilter->GetClassAlias();
-		if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode))
-		{
-			throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}' - the condition will be ignored");
-		}
-		$oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
-		if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass()))
-		{
-			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()}");
-		}
 
 		// Find the node on which the new tree must be attached (most of the time it is "this")
 		$oReceivingFilter = $this->GetNode($this->GetClassAlias());
@@ -1129,7 +1129,7 @@ class DBObjectSearch
 			$sFltCode = $oExpression->GetName();
 			if (empty($sClassAlias))
 			{
-				// Try to find an alias
+				// Need to find the right alias
 				// Build an array of field => array of aliases
 				$aFieldClasses = array();
 				foreach($aClassAliases as $sAlias => $sReal)
@@ -1139,29 +1139,8 @@ class DBObjectSearch
 						$aFieldClasses[$sAnFltCode][] = $sAlias;
 					}
 				}
-				if (!array_key_exists($sFltCode, $aFieldClasses))
-				{
-					throw new OqlNormalizeException('Unknown filter code', $sQuery, $oExpression->GetNameDetails(), array_keys($aFieldClasses));
-				}
-				if (count($aFieldClasses[$sFltCode]) > 1)
-				{
-					throw new OqlNormalizeException('Ambiguous filter code', $sQuery, $oExpression->GetNameDetails());
-				}
 				$sClassAlias = $aFieldClasses[$sFltCode][0];
 			}
-			else
-			{
-				if (!array_key_exists($sClassAlias, $aClassAliases))
-				{
-					throw new OqlNormalizeException('Unknown class [alias]', $sQuery, $oExpression->GetParentDetails(), array_keys($aClassAliases));
-				}
-				$sClass = $aClassAliases[$sClassAlias];
-				if (!MetaModel::IsValidFilterCode($sClass, $sFltCode))
-				{
-					throw new OqlNormalizeException('Unknown filter code', $sQuery, $oExpression->GetNameDetails(), MetaModel::GetFiltersList($sClass));
-				}
-			}
-
 			return new FieldExpression($sFltCode, $sClassAlias);
 		}
 		elseif ($oExpression instanceof VariableOqlExpression)
@@ -1242,15 +1221,13 @@ class DBObjectSearch
 
 		$oOql = new OqlInterpreter($sQuery);
 		$oOqlQuery = $oOql->ParseObjectQuery();
-		
+
+		$oMetaModel = new ModelReflectionRuntime();
+		$oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue
+
 		$sClass = $oOqlQuery->GetClass();
 		$sClassAlias = $oOqlQuery->GetClassAlias();
 
-		if (!MetaModel::IsValidClass($sClass))
-		{
-			throw new UnknownClassOqlException($sQuery, $oOqlQuery->GetClassDetails(), MetaModel::GetClasses());
-		}
-
 		$oResultFilter = new DBObjectSearch($sClass, $sClassAlias);
 		$aAliases = array($sClassAlias => $sClass);
 
@@ -1266,21 +1243,6 @@ class DBObjectSearch
 			{
 				$sJoinClass = $oJoinSpec->GetClass();
 				$sJoinClassAlias = $oJoinSpec->GetClassAlias();
-				if (!MetaModel::IsValidClass($sJoinClass))
-				{
-					throw new UnknownClassOqlException($sQuery, $oJoinSpec->GetClassDetails(), MetaModel::GetClasses());
-				}
-				if (array_key_exists($sJoinClassAlias, $aAliases))
-				{
-					if ($sJoinClassAlias != $sJoinClass)
-					{
-						throw new OqlNormalizeException('Duplicate class alias', $sQuery, $oJoinSpec->GetClassAliasDetails());
-					}
-					else
-					{
-						throw new OqlNormalizeException('Duplicate class name', $sQuery, $oJoinSpec->GetClassDetails());
-					}
-				} 
 
 				// Assumption: ext key on the left only !!!
 				// normalization should take care of this
@@ -1290,32 +1252,17 @@ class DBObjectSearch
 
 				$oRightField = $oJoinSpec->GetRightField();
 				$sToClass = $oRightField->GetParent();
-				$sPKeyDescriptor = $oRightField->GetName();
-				if ($sPKeyDescriptor != 'id')
-				{
-					throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sQuery, $oRightField->GetNameDetails(), array('id'));
-				}
 
 				$aAliases[$sJoinClassAlias] = $sJoinClass;
 				$aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias);
 
-				if (!array_key_exists($sFromClass, $aJoinItems))
-				{
-					throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sQuery, $oLeftField->GetParentDetails(), array_keys($aJoinItems));
-				}
-				if (!array_key_exists($sToClass, $aJoinItems))
-				{
-					throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sQuery, $oRightField->GetParentDetails(), array_keys($aJoinItems));
-				}
-				$aExtKeys = array_keys(MetaModel::GetExternalKeys($aAliases[$sFromClass]));
-				if (!in_array($sExtKeyAttCode, $aExtKeys))
-				{
-					throw new OqlNormalizeException('Unknown external key in join condition (left expression)', $sQuery, $oLeftField->GetNameDetails(), $aExtKeys);
-				}
-
 				if ($sFromClass == $sJoinClassAlias)
 				{
-					$aJoinItems[$sToClass]->AddCondition_ReferencedBy($aJoinItems[$sFromClass], $sExtKeyAttCode);
+					$oReceiver = $aJoinItems[$sToClass];
+					$oNewComer = $aJoinItems[$sFromClass];
+
+					$aAliasTranslation = array();
+					$oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation);
 				}
 				else
 				{
@@ -1350,7 +1297,11 @@ class DBObjectSearch
 						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
 						break;
 					}
-					$aJoinItems[$sFromClass]->AddCondition_PointingTo($aJoinItems[$sToClass], $sExtKeyAttCode, $iOperatorCode);
+					$oReceiver = $aJoinItems[$sFromClass];
+					$oNewComer = $aJoinItems[$sToClass];
+
+					$aAliasTranslation = array();
+					$oReceiver->AddCondition_PointingTo_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode);
 				}
 			}
 		}
@@ -1360,10 +1311,6 @@ class DBObjectSearch
 		foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails)
 		{
 			$sClassToSelect = $oClassDetails->GetValue();
-			if (!array_key_exists($sClassToSelect, $aAliases))
-			{
-				throw new OqlNormalizeException('Unknown class [alias]', $sQuery, $oClassDetails, array_keys($aAliases));
-			}
 			$aSelected[$sClassToSelect] = $aAliases[$sClassToSelect];
 		}
 		$oResultFilter->m_aClasses = $aAliases;

+ 239 - 7
core/oql/oqlquery.class.inc.php

@@ -127,15 +127,38 @@ class OqlJoinSpec
 	}
 }
 
-class BinaryOqlExpression extends BinaryExpression
+interface CheckableExpression
 {
+	/**
+	 * Check the validity of the expression with regard to the data model
+	 * and the query in which it is used
+	 *
+	 * @param ModelReflection $oModelReflection MetaModel to consider
+	 * @param array $aAliases Aliases to class names (for the current query)
+	 * @param string $sSourceQuery For the reporting
+	 * @throws OqlNormalizeException
+	 */	 	
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery);
 }
 
-class ScalarOqlExpression extends ScalarExpression
+class BinaryOqlExpression extends BinaryExpression implements CheckableExpression
 {
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		$this->m_oLeftExpr->Check($oModelReflection, $aAliases, $sSourceQuery);
+		$this->m_oRightExpr->Check($oModelReflection, $aAliases, $sSourceQuery);
+	}
 }
 
-class FieldOqlExpression extends FieldExpression
+class ScalarOqlExpression extends ScalarExpression implements CheckableExpression
+{
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		// a scalar is always fine
+	}
+}
+
+class FieldOqlExpression extends FieldExpression implements CheckableExpression
 {
 	protected $m_oParent;
 	protected $m_oName;
@@ -161,22 +184,84 @@ class FieldOqlExpression extends FieldExpression
 	{
 		return $this->m_oName;
 	}
+
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		$sClassAlias = $this->GetParent();
+		$sFltCode = $this->GetName();
+		if (empty($sClassAlias))
+		{
+			// Try to find an alias
+			// Build an array of field => array of aliases
+			$aFieldClasses = array();
+			foreach($aAliases as $sAlias => $sReal)
+			{
+				foreach($oModelReflection->GetFiltersList($sReal) as $sAnFltCode)
+				{
+					$aFieldClasses[$sAnFltCode][] = $sAlias;
+				}
+			}
+			if (!array_key_exists($sFltCode, $aFieldClasses))
+			{
+				throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), array_keys($aFieldClasses));
+			}
+			if (count($aFieldClasses[$sFltCode]) > 1)
+			{
+				throw new OqlNormalizeException('Ambiguous filter code', $sSourceQuery, $this->GetNameDetails());
+			}
+			$sClassAlias = $aFieldClasses[$sFltCode][0];
+		}
+		else
+		{
+			if (!array_key_exists($sClassAlias, $aAliases))
+			{
+				throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $this->GetParentDetails(), array_keys($aAliases));
+			}
+			$sClass = $aAliases[$sClassAlias];
+			if (!$oModelReflection->IsValidFilterCode($sClass, $sFltCode))
+			{
+				throw new OqlNormalizeException('Unknown filter code', $sSourceQuery, $this->GetNameDetails(), $oModelReflection->GetFiltersList($sClass));
+			}
+		}
+	}
 }
 
-class VariableOqlExpression extends VariableExpression
+class VariableOqlExpression extends VariableExpression implements CheckableExpression
 {
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		// a scalar is always fine
+	}
 }
 
-class ListOqlExpression extends ListExpression
+class ListOqlExpression extends ListExpression implements CheckableExpression
 {
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		foreach ($this->GetItems() as $oItemExpression)
+		{
+			$oItemExpression->Check($oModelReflection, $aAliases, $sSourceQuery);
+		}
+	}
 }
 
-class FunctionOqlExpression extends FunctionExpression
+class FunctionOqlExpression extends FunctionExpression implements CheckableExpression
 {
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		foreach ($this->GetArgs() as $oArgExpression)
+		{
+			$oArgExpression->Check($oModelReflection, $aAliases, $sSourceQuery);
+		}
+	}
 }
 
-class IntervalOqlExpression extends IntervalExpression
+class IntervalOqlExpression extends IntervalExpression implements CheckableExpression
 {
+	public function Check(ModelReflection $oModelReflection, $aAliases, $sSourceQuery)
+	{
+		// an interval is always fine (made of a scalar and unit)
+	}
 }
 
 abstract class OqlQuery
@@ -235,6 +320,153 @@ class OqlObjectQuery extends OqlQuery
 	{
 		return $this->m_oClassAlias;
 	}
+
+	/**
+	 * Recursively check the validity of the expression with regard to the data model
+	 * and the query in which it is used
+	 * 	 	 
+	 * @param ModelReflection $oModelReflection MetaModel to consider	 	
+	 * @throws OqlNormalizeException
+	 */	 	
+	public function Check(ModelReflection $oModelReflection, $sSourceQuery)
+	{
+		$sClass = $this->GetClass();
+		$sClassAlias = $this->GetClassAlias();
+
+		if (!$oModelReflection->IsValidClass($sClass))
+		{
+			throw new UnknownClassOqlException($sSourceQuery, $this->GetClassDetails(), $oModelReflection->GetClasses('bizmodelx'));
+		}
+
+		$aAliases = array($sClassAlias => $sClass);
+
+		$aJoinSpecs = $this->GetJoins();
+		if (is_array($aJoinSpecs))
+		{
+			foreach ($aJoinSpecs as $oJoinSpec)
+			{
+				$sJoinClass = $oJoinSpec->GetClass();
+				$sJoinClassAlias = $oJoinSpec->GetClassAlias();
+				if (!$oModelReflection->IsValidClass($sJoinClass))
+				{
+					throw new UnknownClassOqlException($sSourceQuery, $oJoinSpec->GetClassDetails(), $oModelReflection->GetClasses());
+				}
+				if (array_key_exists($sJoinClassAlias, $aAliases))
+				{
+					if ($sJoinClassAlias != $sJoinClass)
+					{
+						throw new OqlNormalizeException('Duplicate class alias', $sSourceQuery, $oJoinSpec->GetClassAliasDetails());
+					}
+					else
+					{
+						throw new OqlNormalizeException('Duplicate class name', $sSourceQuery, $oJoinSpec->GetClassDetails());
+					}
+				} 
+
+				// Assumption: ext key on the left only !!!
+				// normalization should take care of this
+				$oLeftField = $oJoinSpec->GetLeftField();
+				$sFromClass = $oLeftField->GetParent();
+				$sExtKeyAttCode = $oLeftField->GetName();
+
+				$oRightField = $oJoinSpec->GetRightField();
+				$sToClass = $oRightField->GetParent();
+				$sPKeyDescriptor = $oRightField->GetName();
+				if ($sPKeyDescriptor != 'id')
+				{
+					throw new OqlNormalizeException('Wrong format for Join clause (right hand), expecting an id', $sSourceQuery, $oRightField->GetNameDetails(), array('id'));
+				}
+
+				$aAliases[$sJoinClassAlias] = $sJoinClass;
+
+				if (!array_key_exists($sFromClass, $aAliases))
+				{
+					throw new OqlNormalizeException('Unknown class in join condition (left expression)', $sSourceQuery, $oLeftField->GetParentDetails(), array_keys($aAliases));
+				}
+				if (!array_key_exists($sToClass, $aAliases))
+				{
+					throw new OqlNormalizeException('Unknown class in join condition (right expression)', $sSourceQuery, $oRightField->GetParentDetails(), array_keys($aAliases));
+				}
+				$aExtKeys = array_keys($oModelReflection->GetExternalKeys($aAliases[$sFromClass]));
+				if (!in_array($sExtKeyAttCode, $aExtKeys))
+				{
+					throw new OqlNormalizeException('Unknown external key in join condition (left expression)', $sSourceQuery, $oLeftField->GetNameDetails(), $aExtKeys);
+				}
+
+				if ($sFromClass == $sJoinClassAlias)
+				{
+					$oAttExtKey = $oModelReflection->GetAttributeDef($aAliases[$sFromClass], $sExtKeyAttCode);
+					if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $oAttExtKey->GetTargetClass()))
+					{
+						throw new OqlNormalizeException("The joined class ($aAliases[$sFromClass]) is not compatible with the external key, which is pointing to {$oAttExtKey->GetTargetClass()}", $sSourceQuery, $oLeftField->GetNameDetails());
+					}
+				}
+				else
+				{
+					$sOperator = $oJoinSpec->GetOperator();
+					switch($sOperator)
+					{
+						case '=':
+						$iOperatorCode = TREE_OPERATOR_EQUALS;
+						break;
+						case 'BELOW':
+						$iOperatorCode = TREE_OPERATOR_BELOW;
+						break;
+						case 'BELOW_STRICT':
+						$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
+						break;
+						case 'NOT_BELOW':
+						$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
+						break;
+						case 'NOT_BELOW_STRICT':
+						$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
+						break;
+						case 'ABOVE':
+						$iOperatorCode = TREE_OPERATOR_ABOVE;
+						break;
+						case 'ABOVE_STRICT':
+						$iOperatorCode = TREE_OPERATOR_ABOVE_STRICT;
+						break;
+						case 'NOT_ABOVE':
+						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE;
+						break;
+						case 'NOT_ABOVE_STRICT':
+						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
+						break;
+					}
+					$oAttExtKey = $oModelReflection->GetAttributeDef($aAliases[$sFromClass], $sExtKeyAttCode);
+					if(!$oModelReflection->IsSameFamilyBranch($aAliases[$sToClass], $oAttExtKey->GetTargetClass()))
+					{
+						throw new OqlNormalizeException("The joined class ($aAliases[$sToClass]) is not compatible with the external key, which is pointing to {$oAttExtKey->GetTargetClass()}", $sSourceQuery, $oLeftField->GetNameDetails());
+					}
+					if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
+					{
+						throw new OqlNormalizeException("The specified tree operator $sOperator is not applicable to the key", $sSourceQuery, $oLeftField->GetNameDetails());
+					}
+				}
+			}
+		}
+
+		// Check the select information
+		//
+		$aSelected = array();
+		foreach ($this->GetSelectedClasses() as $oClassDetails)
+		{
+			$sClassToSelect = $oClassDetails->GetValue();
+			if (!array_key_exists($sClassToSelect, $aAliases))
+			{
+				throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases));
+			}
+			$aSelected[$sClassToSelect] = $aAliases[$sClassToSelect];
+		}
+
+		// Check the condition tree
+		//
+		if ($this->m_oCondition instanceof Expression)
+		{
+			$this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery);
+		}
+	}
 }
 
 ?>

+ 3 - 3
pages/ajax.render.php

@@ -739,7 +739,7 @@ try
 		$sDashletId =  utils::ReadParam('dashlet_id', '', false, 'raw_data');
 		if (is_subclass_of($sDashletClass, 'Dashlet'))
 		{
-			$oDashlet = new $sDashletClass($sDashletId);
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
 			$offset = $oPage->start_capture();
 			$oDashlet->DoRender($oPage, true /* bEditMode */, false /* bEnclosingDiv */);
 			$sHtml = addslashes($oPage->end_capture($offset));
@@ -767,7 +767,7 @@ try
 		$aPreviousValues = $aParams['previous_values']; // hash array: 'attr_xxx' => 'old_value'
 		if (is_subclass_of($sDashletClass, 'Dashlet'))
 		{
-			$oDashlet = new $sDashletClass($sDashletId);
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), $sDashletId);
 			$oForm = $oDashlet->GetForm();
 			$aValues = $oForm->ReadParams(); // hash array: 'xxx' => 'new_value'
 			
@@ -867,7 +867,7 @@ EOF
 		
 		if (is_subclass_of($sDashletClass, 'Dashlet'))
 		{
-			$oDashlet = new $sDashletClass(0);
+			$oDashlet = new $sDashletClass(new ModelReflectionRuntime(), 0);
 			$oDashlet->FromParams($aValues);
 
 			ApplicationMenu::LoadAdditionalMenus();

+ 2 - 0
test/test.class.inc.php

@@ -402,6 +402,8 @@ abstract class TestBizModel extends TestHandler
 //	abstract static public function GetBusinessModelFile();
 //	abstract static public function GetConfigFile();
 
+	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
+
 	protected function DoPrepare()
 	{
 		$sConfigFile = APPROOT.$this->GetConfigFile();

+ 8 - 0
test/test.php

@@ -62,6 +62,12 @@ function IsAValidTestClass($sClassName)
 	return true;
 }
 
+function GetTestClassLine($sClassName)
+{
+	$oReflectionClass = new ReflectionClass($sClassName);
+	return $oReflectionClass->getStartLine();
+}
+
 function DisplayEvents($aEvents, $sTitle)
 {
 	echo "<h4>$sTitle</h4>\n";
@@ -122,7 +128,9 @@ else if ($sTodo == 'exec')
 	else
 	{
 		$oTest  = new $sTestClass();
+		$iStartLine = GetTestClassLine($sTestClass);
 		echo "<h3>Testing: ".$oTest->GetName()."</h3>\n";
+		echo "<h6>testlist.inc.php: $iStartLine</h6>\n";
 		$bRes = $oTest->Execute();
 	}
 

+ 138 - 26
test/testlist.inc.php

@@ -255,6 +255,141 @@ class TestOQLParser extends TestFunction
 	}
 }
 
+class TestOQLNormalization extends TestBizModel
+{
+	static public function GetName() {return 'Check OQL normalization';}
+	static public function GetDescription() {return 'Attempts a series of queries, and in particular those with unknown or inconsistent class/attributes. Assumes a very standard installation!';}
+
+	protected function CheckQuery($sQuery, $bIsCorrectQuery)
+	{
+		try
+		{
+			$oSearch = DBObjectSearch::FromOQL($sQuery);
+			self::DumpVariable($sQuery);
+		}
+		catch (OQLNormalizeException $OqlException)
+		{
+			if ($bIsCorrectQuery)
+			{
+				echo "<p>More info on this unexpected failure:<br/>".$OqlException->getHtmlDesc()."</p>\n";
+				throw $OqlException;
+				return false;
+			}
+			else
+			{
+				// Everything is fine :-)
+				echo "<p>More info on this expected failure:<br/>".$OqlException->getHtmlDesc()."</p>\n";
+				return true;
+			}
+		}
+		catch (Exception $e)
+		{
+			if ($bIsCorrectQuery)
+			{
+				echo "<p>More info on this <b>un</b>expected failure:<br/>".htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8')."</p>\n";
+				throw $e;
+				return false;
+			}
+			else
+			{
+				// Everything is fine :-)
+				echo "<p>More info on this expected failure:<br/>".htmlentities($e->getMessage(), ENT_QUOTES, 'UTF-8')."</p>\n";
+				return true;
+			}
+		}
+		// The query was correctly parsed, was it expected to be correct ?
+		if ($bIsCorrectQuery)
+		{
+			return true;
+		}
+		else
+		{
+			throw new UnitTestException("The query '$sQuery' was parsed with success, while it shouldn't (?)");
+			return false;
+		}
+	}
+
+	protected function TestQuery($sQuery, $bIsCorrectQuery)
+	{
+		if (!$this->CheckQuery($sQuery, $bIsCorrectQuery))
+		{
+			return false;
+		}
+		return true;
+	}
+
+	public function DoExecute()
+	{
+		$aQueries = array(
+			'SELECT Contact' => true,
+			'SELECT Contact WHERE nom_de_famille = "foo"' => false,
+			'SELECT Contact AS c WHERE name = "foo"' => true,
+			'SELECT Contact AS c WHERE nom_de_famille = "foo"' => false,
+			'SELECT Contact AS c WHERE c.name = "foo"' => true,
+			'SELECT Contact AS c WHERE Contact.name = "foo"' => false,
+			'SELECT Contact AS c WHERE x.name = "foo"' => false,
+
+			'SELECT RelationProfessionnelle' => false,
+			'SELECT RelationProfessionnelle AS c WHERE name = "foo"' => false,
+
+			// The first query is the base query altered only in one place in the subsequent queries
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.id WHERE p.name LIKE "foo"' => true,
+			'SELECT Person AS p JOIN lnkXXXXXXXXXXXX AS lnk ON lnk.person_id = p.id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON   p.person_id = p.id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON     person_id = p.id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id =   id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.role      = p.id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.team_id   = p.id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id BELOW p.id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.org_id WHERE p.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON p.id = lnk.person_id WHERE p.name LIKE "foo"' => false, // inverted the JOIN spec
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.id WHERE   name LIKE "foo"' => true,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.id WHERE x.name LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.id WHERE p.eman LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.id WHERE   eman LIKE "foo"' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON lnk.person_id = p.id WHERE id = 1' => false,
+			'SELECT Person AS p JOIN lnkPersonToTeam AS lnk ON p.id = lnk.person_id WHERE p.name LIKE "foo"' => false,
+
+			'SELECT Person AS p JOIN Organization AS o ON p.org_id = o.id WHERE p.name LIKE "foo" AND o.name LIKE "land"' => true,
+			'SELECT Person AS p JOIN Organization AS o ON p.location_id = o.id WHERE p.name LIKE "foo" AND o.name LIKE "land"' => false,
+			'SELECT Person AS p JOIN Organization AS o ON p.name = o.id WHERE p.name LIKE "foo" AND o.name LIKE "land"' => false,
+
+			'SELECT Person AS p JOIN Organization AS o ON      p.org_id = o.id JOIN Person AS p ON      p.org_id = o.id' => false,
+			'SELECT Person      JOIN Organization AS o ON Person.org_id = o.id JOIN Person      ON Person.org_id = o.id' => false,
+
+			'SELECT Person AS p JOIN Location AS l ON p.location_id = l.id' => true,
+			'SELECT Person AS p JOIN Location AS l ON p.location_id BELOW l.id' => false,
+
+			'SELECT Person FROM Person JOIN Location ON Person.location_id = Location.id' => true,
+			'SELECT p FROM Person AS p JOIN Location AS l ON p.location_id = l.id' => true,
+			'SELECT l FROM Person AS p JOIN Location AS l ON p.location_id = l.id' => true,
+			'SELECT l, p FROM Person AS p JOIN Location AS l ON p.location_id = l.id' => true,
+			'SELECT p, l FROM Person AS p JOIN Location AS l ON p.location_id = l.id' => true,
+			'SELECT foo FROM Person AS p JOIN Location AS l ON p.location_id = l.id' => false,
+			'SELECT p, foo FROM Person AS p JOIN Location AS l ON p.location_id = l.id' => false,
+		);
+
+		$iErrors = 0;
+
+		foreach($aQueries as $sQuery => $bIsCorrectQuery)
+		{
+			$sIsOk = $bIsCorrectQuery ? 'good' : 'bad';
+			echo "<h4>Testing query: $sQuery ($sIsOk)</h4>\n";
+			try
+			{
+				$bRet = $this->TestQuery($sQuery, $bIsCorrectQuery);
+			}
+			catch(Exception $e)
+			{
+				$this->m_aErrors[] = $e->getMessage();
+				$bRet = false;
+			}
+			if (!$bRet) $iErrors++;
+		}
+		
+		return ($iErrors == 0);
+	}
+}
 
 class TestCSVParser extends TestFunction
 {
@@ -1127,8 +1262,6 @@ class TestItopEfficiency extends TestBizModel
 		return 'Measure time to perform the queries';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoBenchmark($sOqlQuery)
 	{
 		echo "<h3>Testing query: $sOqlQuery</h3>";
@@ -1252,8 +1385,6 @@ class TestQueries extends TestBizModel
 		return 'Try as many queries as possible';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoBenchmark($sOqlQuery)
 	{
 		echo "<h5>Testing query: $sOqlQuery</h5>";
@@ -1314,9 +1445,9 @@ class TestQueries extends TestBizModel
 			'SELECT Person AS PP WHERE PP.friendlyname LIKE "%dali"',
 			'SELECT Person AS PP WHERE PP.location_id_friendlyname LIKE "%ce ch%"',
 			'SELECT Organization AS OO JOIN Person AS PP ON PP.org_id = OO.id',
-			'SELECT lnkTeamToContact AS lnk JOIN Team AS T ON lnk.team_id = T.id',
-			'SELECT lnkTeamToContact AS lnk JOIN Team AS T ON lnk.team_id = T.id JOIN Contact AS C ON lnk.contact_id = C.id',
-			'SELECT Incident JOIN Person ON Incident.agent_id = Person.id WHERE Person.id = 5',
+			'SELECT lnkPersonToTeam AS lnk JOIN Team AS T ON lnk.team_id = T.id',
+			'SELECT lnkPersonToTeam AS lnk JOIN Team AS T ON lnk.team_id = T.id JOIN Person AS p ON lnk.person_id = p.id',
+			'SELECT UserRequest AS ur JOIN Person ON ur.agent_id = Person.id WHERE Person.id = 5',
 			// this one is failing...
 			//'SELECT L, P FROM Person AS P JOIN Location AS L ON P.location_id = L.id',
 		);
@@ -1364,8 +1495,6 @@ class TestQueriesByAPI extends TestBizModel
 		return 'Validate the DBObjectSearch API, through a set of complex (though realistic cases)';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoExecute()
 	{
 		// Note: relying on eval() - after upgrading to PHP 5.3 we can move to closure (aka anonymous functions)
@@ -1464,9 +1593,6 @@ class TestItopBulkLoad extends TestBizModel
 		return 'Execute a bulk change at the Core API level';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
-	
 	protected function DoExecute()
 	{
 		$sLogin = 'testbulkload_'.time();
@@ -2033,8 +2159,6 @@ class TestDataExchange extends TestBizModel
 		return 'Test REST services: synchro_import and synchro_exec';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoExecScenario($aSingleScenario)
 	{
 		echo "<div style=\"padding: 10;\">\n";
@@ -3037,8 +3161,6 @@ abstract class TestSoapDirect extends TestBizModel
 	static public function GetName() {return 'Test web services locally';}
 	static public function GetDescription() {return 'Invoke the service directly (troubleshooting)';}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected $m_aTestSpecs;
 
 	protected function DoExecute()
@@ -3137,8 +3259,6 @@ class TestTriggerAndEmail extends TestBizModel
 	static public function GetName() {return 'Test trigger and email';}
 	static public function GetDescription() {return 'Create a trigger and an email, then activates the trigger';}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function CreateEmailSpec($oTrigger, $sStatus, $sTo, $sCC, $sTesterEmail)
 	{
 		$oAction = MetaModel::NewObject("ActionEmail");
@@ -3281,8 +3401,6 @@ class TestDBProperties extends TestBizModel
 		return 'Write and read a dummy property';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoExecute()
 	{
 		$sName = 'test';
@@ -3304,8 +3422,6 @@ class TestCreateObjects extends TestBizModel
 		return 'Create weird objects (reproduce a bug?)';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoExecute()
 	{
 		$oMyObj = MetaModel::NewObject("Server");
@@ -3365,8 +3481,6 @@ class TestSetLinkset extends TestBizModel
 		return 'Create a user account, setting its profile by the mean of a string (prerequisite to CSV import of linksets)';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoExecute()
 	{
 		$oUser = new UserLocal();
@@ -3399,8 +3513,6 @@ class TestEmailAsynchronous extends TestBizModel
 		return 'Queues a request to send an email';
 	}
 
-	static public function GetConfigFile() {return 'conf/production/config-itop.php';}
-
 	protected function DoExecute()
 	{
 		for ($i = 0 ; $i < 2 ; $i++)