- Fixed the combination of the "PointingTo" conditions, especially when combining several "BELOW" conditions for the same criteria (used for security and context purposes)

- Make sure that the "security" limitiation provided by GetSelectFilter is applied only once to a given filter to avoid the creation of unneeded complex queries

SVN:trunk[1393]
This commit is contained in:
Denis Flaven
2011-07-31 09:01:45 +00:00
parent ee3652acad
commit bfddec242b
2 changed files with 159 additions and 133 deletions

View File

@@ -39,6 +39,7 @@ class DBObjectSearch
private $m_aPointingTo;
private $m_aReferencedBy;
private $m_aRelatedTo;
private $m_bDataFiltered;
// By default, some information may be hidden to the current user
// But it may happen that we need to disable that feature
@@ -59,10 +60,13 @@ class DBObjectSearch
$this->m_aPointingTo = array();
$this->m_aReferencedBy = array();
$this->m_aRelatedTo = array();
$this->m_bDataFiltered = false;
}
public function AllowAllData() {$this->m_bAllowAllData = true;}
public function IsAllDataAllowed() {return $this->m_bAllowAllData;}
public function IsDataFiltered() {return $this->m_bDataFiltered; }
public function SetDataFiltered() {$this->m_bDataFiltered = true;}
public function GetClassName($sAlias) {return $this->m_aClasses[$sAlias];}
public function GetJoinedClasses() {return $this->m_aClasses;}
@@ -247,10 +251,11 @@ class DBObjectSearch
protected function TransferConditionExpression($oFilter, $aTranslation)
{
$oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */);
//echo "<p>TransferConditionExpression:<br/>";
//echo "Adding Conditions:<br/><pre>".print_r($oTranslated, true)."</pre>\n";
//echo "Adding Conditions:<br/><pre>oFilter:\n".print_r($oFilter, true)."\naTranslation:\n".print_r($aTranslation, true)."</pre>\n";
//echo "</p>";
$oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */);
//echo "Adding Conditions (translated):<br/><pre>".print_r($oTranslated, true)."</pre>\n";
$this->AddConditionExpression($oTranslated);
// #@# what about collisions in parameter names ???
$this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams);
@@ -441,32 +446,38 @@ class DBObjectSearch
);
}
protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation)
protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true)
{
$sOrigAlias = $this->GetClassAlias();
if (array_key_exists($sOrigAlias, $aClassAliases))
if ($bTranslateMainAlias)
{
$sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetClass());
//echo "<h1>Generating a new alias for $sOrigAlias (already used). It is now: $sNewAlias</h1>\n";
$this->m_aSelectedClasses[$sNewAlias] = $this->GetClass();
unset($this->m_aSelectedClasses[$sOrigAlias]);
$this->m_aClasses[$sNewAlias] = $this->GetClass();
unset($this->m_aClasses[$sOrigAlias]);
// Translate the condition expression with the new alias
$aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias;
$sOrigAlias = $this->GetClassAlias();
if (array_key_exists($sOrigAlias, $aClassAliases))
{
$sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetClass());
//echo "<p>Generating a new alias for $sOrigAlias (already used). It is now: $sNewAlias</p>\n";
$this->m_aSelectedClasses[$sNewAlias] = $this->GetClass();
unset($this->m_aSelectedClasses[$sOrigAlias]);
$this->m_aClasses[$sNewAlias] = $this->GetClass();
unset($this->m_aClasses[$sOrigAlias]);
// Translate the condition expression with the new alias
$aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias;
}
//echo "<p>Adding the alias ".$this->GetClass()." as ".$this->GetClassAlias()."</p>\n";
// add the alias into the filter aliases list
$aClassAliases[$this->GetClassAlias()] = $this->GetClass();
}
//echo "<h1>Adding the alias for ".$this->GetClass().". as ".$this->GetClassAlias()."</h1>\n";
// add the alias into the filter aliases list
$aClassAliases[$this->GetClassAlias()] = $this->GetClass();
foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $oFilter)
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
$oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
foreach($aFilter as $sAlias => $oFilter)
{
$oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
}
}
}
@@ -489,7 +500,7 @@ class DBObjectSearch
protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
{
//echo "<p style=\"color:green\">Calling: AddCondition_PointingTo_InNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
//echo "<p style=\"color:green\">Calling: AddCondition_PointingTo_InNameSpace([<pre>".print_r($aClassAliases, true)."</pre></br>], [<pre>".print_r($aAliasTranslation, true)."</pre>]);</p>";
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");
@@ -504,31 +515,36 @@ class DBObjectSearch
throw new CoreException("The specified tree operator $isOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
}
$bSamePointingTo = false;
if (array_key_exists($sExtKeyAttCode, $this->m_aPointingTo))
{
if (array_key_exists($iOperatorCode, $this->m_aPointingTo[$sExtKeyAttCode]))
{
// Same ext key and same operator, merge the filters together
$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation);
}
else
{
//echo "<p style=\"color:red\">Calling: AddToNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] = $oFilter;
if (array_key_exists($oFilter->GetClassAlias(), $this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode]))
{
//echo "<p style=\"color:red\">[".__LINE__."]this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][".$oFilter->GetFirstJoinedClassAlias()."]:<pre>\n".print_r($this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode], true)."</pre>;</p>";
$bSamePointingTo = true;
}
}
}
//echo "<p style=\"color:red\">[".__LINE__."]Calling: AddToNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
if ($bSamePointingTo)
{
//echo "<p style=\"color:red\">[".__LINE__."]AddPointingTo: Merging filters for [$sExtKeyAttCode][$iOperatorCode][".$oFilter->GetClassAlias()."]</p>";
// Same ext key, alias and same operator, merge the filters together
// $sAlias = $oFilter->GetClassAlias();
//echo "<p style=\"color:red\">[".__LINE__."]before: AddToNameSpace(aClassAliases[<pre>\n".print_r($aClassAliases, true)."</pre>], aAliasTranslation[<pre>\n".print_r($aAliasTranslation, true)."</pre>]);</p>";
$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation, true /* Don't translate the main alias */);
//echo "<p style=\"color:blue\">[".__LINE__."]after: AddToNameSpace(aClassAliases[<pre>\n".print_r($aClassAliases, true)."</pre>], aAliasTranslation[<pre>\n".print_r($aAliasTranslation, true)."</pre>]);</p>";
// $this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][$sAlias]->MergeWith($oFilter, $aClassAliases, $aAliasTranslation);
$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][$oFilter->GetClassAlias()] = $oFilter;
}
else
{
//echo "<p style=\"color:red\">Calling: AddToNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
//echo "<p style=\"color:red\">[".__LINE__."]AddPointingTo: Adding a new PointingTo filter for [$sExtKeyAttCode][$iOperatorCode][".$oFilter->GetClassAlias()."]</p>";
$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
// #@# The condition expression found in that filter should not be used - could be another kind of structure like a join spec tree !!!!
// $oNewFilter = clone $oFilter;
// $oNewFilter->ResetCondition();
$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] = $oFilter;
$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][$oFilter->GetClassAlias()] = $oFilter;
}
}
@@ -607,9 +623,12 @@ class DBObjectSearch
foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $oExtFilter)
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
$this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode);
foreach($aFilter as $sAlias => $oExtFilter)
{
$this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode);
}
}
}
foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences)
@@ -797,33 +816,36 @@ class DBObjectSearch
$sRes = '';
foreach($this->m_aPointingTo as $sExtKey => $aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $oFilter)
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
switch($iOperatorCode)
foreach($aFilter as $sAlias => $oFilter)
{
case TREE_OPERATOR_EQUALS:
$sOperator = ' = ';
break;
case TREE_OPERATOR_BELOW:
$sOperator = ' BELOW ';
break;
case TREE_OPERATOR_BELOW_STRICT:
$sOperator = ' BELOW STRICT ';
break;
case TREE_OPERATOR_NOT_BELOW:
$sOperator = ' NOT BELOW ';
break;
case TREE_OPERATOR_NOT_BELOW_STRICT:
$sOperator = ' NOT BELOW STRICT ';
break;
switch($iOperatorCode)
{
case TREE_OPERATOR_EQUALS:
$sOperator = ' = ';
break;
case TREE_OPERATOR_BELOW:
$sOperator = ' BELOW ';
break;
case TREE_OPERATOR_BELOW_STRICT:
$sOperator = ' BELOW STRICT ';
break;
case TREE_OPERATOR_NOT_BELOW:
$sOperator = ' NOT BELOW ';
break;
case TREE_OPERATOR_NOT_BELOW_STRICT:
$sOperator = ' NOT BELOW STRICT ';
break;
}
$sRes .= ' JOIN '.$oFilter->GetClass().' AS '.$oFilter->GetClassAlias().' ON '.$this->GetClassAlias().'.'.$sExtKey.$sOperator.$oFilter->GetClassAlias().'.id';
$sRes .= $oFilter->ToOQL_Joins();
}
$sRes .= ' JOIN '.$oFilter->GetClass().' AS '.$oFilter->GetClassAlias().' ON '.$this->GetClassAlias().'.'.$sExtKey.$sOperator.$oFilter->GetClassAlias().'.id';
$sRes .= $oFilter->ToOQL_Joins();
}
}
foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)

View File

@@ -1811,7 +1811,7 @@ if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]))
{
// Hide objects that are not visible to the current user
//
if (!$oFilter->IsAllDataAllowed())
if (!$oFilter->IsAllDataAllowed() && !$oFilter->IsDataFiltered())
{
$oVisibleObjects = UserRights::GetSelectFilter($oFilter->GetClass());
if ($oVisibleObjects === false)
@@ -1822,6 +1822,7 @@ if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]))
if (is_object($oVisibleObjects))
{
$oFilter->MergeWith($oVisibleObjects);
$oFilter->SetDataFiltered();
}
else
{
@@ -2423,7 +2424,7 @@ if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]))
$sKeyClassAlias = self::GenerateUniqueAlias($aClassAliases, $sKeyClass.'_'.$sKeyAttCode, $sKeyClass);
$oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias);
$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS] = $oExtFilter;
$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter;
}
}
}
@@ -2431,94 +2432,97 @@ if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]))
foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $oExtFilter)
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it
// The aliases should not conflict because normalization occured while building the filter
$oKeyAttDef = self::GetAttributeDef($sTableClass, $sKeyAttCode);
$sKeyClass = $oExtFilter->GetFirstJoinedClass();
$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
foreach($aFilter as $sAlias => $oExtFilter)
{
if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it
// The aliases should not conflict because normalization occured while building the filter
$oKeyAttDef = self::GetAttributeDef($sTableClass, $sKeyAttCode);
$sKeyClass = $oExtFilter->GetFirstJoinedClass();
$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
//echo "MAKEQUERY-$sTableClass::$sKeyAttCode Foreach PointingTo($iOperatorCode) <span style=\"color:red\">$sKeyClass (alias:$sKeyClassAlias)</span><br/>\n";
// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree
// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree
if ($iOperatorCode == TREE_OPERATOR_EQUALS)
{
// Specify expected attributes for the target class query
// ... and use the current alias !
$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
//echo "MAKEQUERY-array_key_exists($sTableClass, \$aExtKeys)<br/>\n";
if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
if ($iOperatorCode == TREE_OPERATOR_EQUALS)
{
foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
// Specify expected attributes for the target class query
// ... and use the current alias !
$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
//echo "MAKEQUERY-array_key_exists($sTableClass, \$aExtKeys)<br/>\n";
if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
{
foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
{
//echo "MAKEQUERY aExtKeys[$sTableClass][$sKeyAttCode] => $sAttCode-oAtt: <pre>".print_r($oAtt, true)."</pre><br/>\n";
if ($oAtt instanceof AttributeFriendlyName)
{
// Note: for a given ext key, there is one single attribute "friendly name"
$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);
//echo "<p><b>aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);</b></p>\n";
}
else
{
$sExtAttCode = $oAtt->GetExtAttCode();
// Translate mainclass.extfield => remoteclassalias.remotefieldcode
$oRemoteAttDef = self::GetAttributeDef($sKeyClass, $sExtAttCode);
foreach ($oRemoteAttDef->GetSQLExpressions() as $sColID => $sRemoteAttExpr)
if ($oAtt instanceof AttributeFriendlyName)
{
$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
//echo "<p><b>aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);</b></p>\n";
// Note: for a given ext key, there is one single attribute "friendly name"
$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);
//echo "<p><b>aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);</b></p>\n";
}
else
{
$sExtAttCode = $oAtt->GetExtAttCode();
// Translate mainclass.extfield => remoteclassalias.remotefieldcode
$oRemoteAttDef = self::GetAttributeDef($sKeyClass, $sExtAttCode);
foreach ($oRemoteAttDef->GetSQLExpressions() as $sColID => $sRemoteAttExpr)
{
$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
//echo "<p><b>aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);</b></p>\n";
}
//echo "<p><b>ExtAttr2: $sTargetAlias.$sAttCode to $sKeyClassAlias.$sRemoteAttExpr (class: $sKeyClass)</b></p>\n";
}
}
}
// Translate prior to recursing
//
// Translate prior to recursing
//
//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."\n".print_r($aTranslateNow, true)."</pre></p>\n";
$oQBExpr->Translate($aTranslateNow, false);
$oQBExpr->Translate($aTranslateNow, false);
//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
//echo "<p>External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()/p>\n";
self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()");
$oQBExpr->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()");
$oQBExpr->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
//echo "<p>Recursive MakeQuery ".__LINE__.": <pre>\n".print_r($aSelectedClasses, true)."</pre></p>\n";
$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
$oJoinExpr = $oQBExpr->PopJoinField();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
$sLocalKeyField = current($aCols); // get the first column for an external key
self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
if ($oKeyAttDef->IsNullAllowed())
{
$oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
}
else
{
$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
$oJoinExpr = $oQBExpr->PopJoinField();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
$sLocalKeyField = current($aCols); // get the first column for an external key
self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
if ($oKeyAttDef->IsNullAllowed())
{
$oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
}
else
{
$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
}
}
}
}
else
{
$oQBExpr->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
$oJoinExpr = $oQBExpr->PopJoinField();
else
{
$oQBExpr->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
$oJoinExpr = $oQBExpr->PopJoinField();
//echo "MAKEQUERY-PopJoinField pour $sKeyAttCode, $sKeyClassAlias: <pre>".print_r($oJoinExpr, true)."</pre><br/>\n";
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft()
$sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight()
$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft()
$sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight()
$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
//echo "MAKEQUERY-LocalKeyLeft pour $sKeyAttCode => $LocalKeyLeft<br/>\n";
$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
}
}
}
}