OQL normalization and dashlets have been made independent from the class MetaModel

Added OQL normalization unit tests (to be run on a standard installation)

SVN:trunk[2767]
This commit is contained in:
Romain Quetiez
2013-06-03 13:26:14 +00:00
parent 69c37b07de
commit 26db86beb2
8 changed files with 471 additions and 167 deletions

View File

@@ -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']);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
}
}
?>

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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++)