Переглянути джерело

Obsolescence: introduction of this new concept (wording could change later). The obsolescence of an object is computed after other attributes, by the mean of an OQL expression. The code has been refactored (again) so as to factorize between the computation of friendly names and the computation of obsolescence flags. The refactoring involved a significant AND RISKY change: external key friendly names (magic attributes) have been changed from AttributeFriendlyName to AttributeExternalField, which simplifies the SQL query build logic.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4732 a333f486-631f-4898-b8df-5754b55c2be0
romainq 8 роки тому
батько
коміт
4b36e9e017

+ 3 - 6
application/cmdbabstract.class.inc.php

@@ -1381,15 +1381,12 @@ EOF
 							$rawValue = $oObj->Get($sAttCodeEx);
 							// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
 							// let's fix this and make sure we render an empty string if the key == 0
-							if ($oAttDef instanceof AttributeFriendlyName)
+							if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName())
 							{
 								$sKeyAttCode = $oAttDef->GetKeyAttCode();
-								if ($sKeyAttCode != 'id')
+								if ($oObj->Get($sKeyAttCode) == 0)
 								{
-									if ($oObj->Get($sKeyAttCode) == 0)
-									{
-										$rawValue = '';
-									}
+									$rawValue = '';
 								}
 							}
 							if ($bLocalize)

+ 9 - 2
application/datatable.class.inc.php

@@ -928,8 +928,15 @@ class DataTableSettings implements Serializable
 			}
 			else if ($oAttDef->IsExternalField())
 			{
-				$oExtAttDef = $oAttDef->GetExtAttDef();
-				$sLabel = Dict::Format('UI:ExtField_AsRemoteField', $oAttDef->GetLabel(), $oExtAttDef->GetLabel());
+				if ($oAttDef->IsFriendlyName())
+				{
+					$sLabel = Dict::Format('UI:ExtKey_AsFriendlyName', $oAttDef->GetLabel());
+				}
+				else
+				{
+					$oExtAttDef = $oAttDef->GetExtAttDef();
+					$sLabel = Dict::Format('UI:ExtField_AsRemoteField', $oAttDef->GetLabel(), $oExtAttDef->GetLabel());
+				}
 			}
 			elseif ($oAttDef instanceof AttributeFriendlyName)
 			{

+ 100 - 91
core/attributedef.class.inc.php

@@ -239,7 +239,7 @@ abstract class AttributeDefinition
 	 */
 	static public function IsBasedOnDBColumns() {return false;}
 	/**
-	 * Returns true if the attribute value is built after other attributes by the mean of an expression
+	 * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via GetOQLExpression)
 	 * @return bool
 	 */
 	static public function IsBasedOnOQLExpression() {return false;}
@@ -926,9 +926,12 @@ class AttributeLinkedSet extends AttributeDefinition
 						}
 					}
 					if ($sAttCode == $this->GetExtKeyToMe()) continue;
-					if ($oAttDef->IsExternalField() && ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe())) continue;
-					if (($oAttDef instanceof AttributeFriendlyName) && ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe())) continue;
-					if (($oAttDef instanceof AttributeFriendlyName) && ($oAttDef->GetKeyAttCode() == 'id')) continue;
+					if ($oAttDef->IsExternalField())
+					{
+						if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) continue;
+						if ($oAttDef->IsFriendlyName()) continue;
+					}
+					if ($oAttDef instanceof AttributeFriendlyName) continue;
 					if (!$oAttDef->IsScalar()) continue;
 					$sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize);
 					$sRes .= "<$sAttCode>$sAttValue</$sAttCode>\n";
@@ -4826,11 +4829,20 @@ class AttributeExternalField extends AttributeDefinition
 
 	public function GetLabel($sDefault = null)
 	{
-		$sLabel = parent::GetLabel('');
-		if (strlen($sLabel) == 0)
+		if ($this->IsFriendlyName())
 		{
-			$oRemoteAtt = $this->GetExtAttDef();
-			$sLabel = $oRemoteAtt->GetLabel($this->m_sCode);
+			$sKeyAttCode = $this->Get("extkey_attcode");
+			$oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
+			$sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
+		}
+		else
+		{
+			$sLabel = parent::GetLabel('');
+			if (strlen($sLabel) == 0)
+			{
+				$oRemoteAtt = $this->GetExtAttDef();
+				$sLabel = $oRemoteAtt->GetLabel($this->m_sCode);
+			}
 		}
 		return $sLabel;
 	}
@@ -4872,6 +4884,27 @@ class AttributeExternalField extends AttributeDefinition
 		}
 	}
 
+	/**
+	 * @return bool
+	 */
+	public function IsFriendlyName()
+	{
+		$oRemoteAtt = $this->GetExtAttDef();
+		if ($oRemoteAtt instanceof AttributeExternalField)
+		{
+			$bRet = $oRemoteAtt->IsFriendlyName();
+		}
+		elseif ($oRemoteAtt instanceof  AttributeFriendlyName)
+		{
+			$bRet = true;
+		}
+		else
+		{
+			$bRet = false;
+		}
+		return $bRet;
+	}
+
 	public function GetTargetClass($iType = EXTKEY_RELATIVE)
 	{
 		return $this->GetKeyAttDef($iType)->GetTargetClass();
@@ -6700,12 +6733,11 @@ class AttributePropertySet extends AttributeTable
  */
 class AttributeFriendlyName extends AttributeDefinition
 {
-	public function __construct($sCode, $sExtKeyAttCode)
+	public function __construct($sCode)
 	{
 		$this->m_sCode = $sCode;
 		$aParams = array();
 		$aParams["default_value"] = '';
-		$aParams["extkey_attcode"] = $sExtKeyAttCode;
 		parent::__construct($sCode, $aParams);
 
 		$this->m_sValue = $this->Get("default_value");
@@ -6732,84 +6764,15 @@ class AttributeFriendlyName extends AttributeDefinition
 	static public function IsBasedOnOQLExpression() {return true;}
 	public function GetOQLExpression()
 	{
-		return static::GetExtendedNameExpression($this->GetHostClass());
-	}
-	/**
-	 *	Get the friendly name for the class and its subclasses (if finalclass = 'subclass' ...)
-	 *	Simplifies the final expression by grouping classes having the same name expression
-	 *	Used when querying a parent class
-	 */
-	static protected function GetExtendedNameExpression($sClass)
-	{
-		// 1st step - get all of the required expressions (instantiable classes)
-		//            and group them using their OQL representation
-		//
-		$aFNExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes)
-		foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass)
-		{
-			if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue;
-
-			$oSubClassName = MetaModel::GetNameExpression($sSubClass);
-			$sSignature = $oSubClassName->Render();
-			if (!array_key_exists($sSignature, $aFNExpressions))
-			{
-				$aFNExpressions[$sSignature] = array(
-					'expression' => $oSubClassName,
-					'classes' => array(),
-				);
-			}
-			$aFNExpressions[$sSignature]['classes'][] = $sSubClass;
-		}
-
-		// 2nd step - build the final name expression depending on the finalclass
-		//
-		if (count($aFNExpressions) == 1)
-		{
-			$aExpData = reset($aFNExpressions);
-			$oNameExpression = $aExpData['expression'];
-		}
-		else
-		{
-			$oNameExpression = null;
-			foreach ($aFNExpressions as $sSignature => $aExpData)
-			{
-				$oClassListExpr = ListExpression::FromScalars($aExpData['classes']);
-				$oClassExpr = new FieldExpression('finalclass', $sClass);
-				$oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
-
-				if (is_null($oNameExpression))
-				{
-					$oNameExpression = $aExpData['expression'];
-				}
-				else
-				{
-					$oNameExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oNameExpression));
-				}
-			}
-		}
-		return $oNameExpression;
+		return MetaModel::GetNameExpression($this->GetHostClass());
 	}
 
-
-	public function GetKeyAttCode() {return $this->Get("extkey_attcode");} 
-
-	public function GetExtAttCode() {return 'friendlyname';} 
-
 	public function GetLabel($sDefault = null)
 	{
 		$sLabel = parent::GetLabel('');
 		if (strlen($sLabel) == 0)
 		{
-			$sKeyAttCode = $this->Get("extkey_attcode");
-			if ($sKeyAttCode == 'id')
-			{
-				return Dict::S('Core:FriendlyName-Label');
-			}
-			else
-			{
-				$oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
-				$sLabel = $oExtKeyAttDef->GetLabel($this->m_sCode);
-			}
+			$sLabel = Dict::S('Core:FriendlyName-Label');
 		}
 		return $sLabel;
 	}
@@ -6818,16 +6781,7 @@ class AttributeFriendlyName extends AttributeDefinition
 		$sLabel = parent::GetDescription('');
 		if (strlen($sLabel) == 0)
 		{
-			$sKeyAttCode = $this->Get("extkey_attcode");
-			if ($sKeyAttCode == 'id')
-			{
-				return Dict::S('Core:FriendlyName-Description');
-			}
-			else
-			{
-				$oExtKeyAttDef = MetaModel::GetAttributeDef($this->GetHostClass(), $sKeyAttCode);
-				$sLabel = $oExtKeyAttDef->GetDescription('');
-			}
+			$sLabel = Dict::S('Core:FriendlyName-Description');
 		}
 		return $sLabel;
 	} 
@@ -7712,3 +7666,58 @@ class AttributeArchiveDate extends AttributeDate
 		return parent::GetDescription($sDefault);
 	}
 }
+
+class AttributeObsolescenceFlag extends AttributeBoolean
+{
+	public function __construct($sCode)
+	{
+		parent::__construct($sCode, array("allowed_values"=>null, "sql"=>$sCode, "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array()));
+	}
+	public function IsWritable()
+	{
+		return false;
+	}
+	public function IsMagic()
+	{
+		return true;
+	}
+
+	static public function IsBasedOnDBColumns() {return false;}
+	/**
+	 * Returns true if the attribute value is built after other attributes by the mean of an expression (obtained via GetOQLExpression)
+	 * @return bool
+	 */
+	static public function IsBasedOnOQLExpression() {return true;}
+	public function GetOQLExpression()
+	{
+		return MetaModel::GetObsolescenceExpression($this->GetHostClass());
+	}
+
+	public function GetSQLExpressions($sPrefix = '')
+	{
+		return array();
+		if ($sPrefix == '')
+		{
+			$sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property
+		}
+		return array('' => $sPrefix);
+	}
+	public function GetSQLColumns($bFullSpec = false) {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
+	public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
+
+	public function GetEditClass() {return "";}
+
+	public function GetValuesDef() {return null;}
+	public function GetPrerequisiteAttributes($sClass = null) {return $this->GetOptional("depends_on", array());}
+
+	public function IsDirectField() {return true;}
+	static public function IsScalar() {return true;}
+	public function GetSQLExpr()
+	{
+		return null;
+	}
+
+	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);}
+	public function IsNullAllowed() {return false;}
+}
+

+ 14 - 4
core/dbobject.class.php

@@ -410,7 +410,7 @@ abstract class DBObject implements iDisplay
 
 				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
 				{
-					if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sAttCode))
+					if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
 					{
 						$this->m_aCurrValues[$sCode] = $value->Get($oDef->GetExtAttCode());
 						$this->m_aLoadedAtt[$sCode] = true;
@@ -423,7 +423,7 @@ abstract class DBObject implements iDisplay
 				// Invalidate the corresponding fields so that they get reloaded in case they are needed (See Get())
 				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
 				{
-					if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sAttCode))
+					if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sAttCode))
 					{
 						$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue($this);
 						unset($this->m_aLoadedAtt[$sCode]);
@@ -566,7 +566,7 @@ abstract class DBObject implements iDisplay
 			else
 			{
 				// Not loaded... is it related to an external key?
-				if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+				if ($oAttDef->IsExternalField())
 				{
 					// Let's get the object and compute all of the corresponding attributes
 					// (i.e not only the requested attribute)
@@ -588,7 +588,7 @@ abstract class DBObject implements iDisplay
 	
 					foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
 					{
-						if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sExtKeyAttCode))
+						if ($oDef->IsExternalField() && ($oDef->GetKeyAttCode() == $sExtKeyAttCode))
 						{
 							if ($oRemote)
 							{
@@ -3614,6 +3614,16 @@ abstract class DBObject implements iDisplay
 		return $bRet;
 	}
 
+	public function IsObsolete()
+	{
+		$bRet = false;
+		if (MetaModel::IsValidAttCode(get_class($this), 'obsolescence_flag') && $this->Get('obsolescence_flag'))
+		{
+			$bRet = true;
+		}
+		return $bRet;
+	}
+
 	/**
 	 * @param $bArchive
 	 * @throws Exception

+ 92 - 38
core/dbobjectsearch.class.php

@@ -1677,15 +1677,15 @@ class DBObjectSearch extends DBSearch
 		}
 
 		$aFNJoinAlias = array(); // array of (subclass => alias)
-		foreach ($aExpectedAtts as $sAttCode => $oExpression)
+		foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression)
 		{
-			if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) continue;
-			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue;
+			$oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode);
 			if ($oAttDef->IsBasedOnOQLExpression())
 			{
 				// To optimize: detect a restriction on child classes in the condition expression
 				//    e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine')
-				$oExpression = $oAttDef->GetOQLExpression($sClass);
+				$oExpression = static::GetPolymorphicExpression($sClass, $sExpectedAttCode);
 
 				$aRequiredFields = array();
 				$oExpression->GetUnresolvedFields('', $aRequiredFields);
@@ -1700,7 +1700,7 @@ class DBObjectSearch extends DBSearch
 							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
 							$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
 						}
-						elseif ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+						elseif ($oAttDef->IsExternalField())
 						{
 							$sKeyAttCode = $oAttDef->GetKeyAttCode();
 							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
@@ -1739,7 +1739,7 @@ class DBObjectSearch extends DBSearch
 				$oExpression = $oExpression->Translate($aTranslateFields, false);
 
 				$aTranslateNow = array();
-				$aTranslateNow[$sClassAlias]['friendlyname'] = $oExpression;
+				$aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression;
 				$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
 			}
 		}
@@ -1747,17 +1747,14 @@ class DBObjectSearch extends DBSearch
 		// Add the ext fields used in the select (eventually adds an external key)
 		foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
 		{
-			if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+			if ($oAttDef->IsExternalField())
 			{
 				if (array_key_exists($sAttCode, $aExpectedAtts))
 				{
+					// Add the external attribute
 					$sKeyAttCode = $oAttDef->GetKeyAttCode();
-					if ($sKeyAttCode != 'id')
-					{
-						// Add the external attribute
-						$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
-						$aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef;
-					}
+					$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
+					$aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef;
 				}
 			}
 		}
@@ -1871,7 +1868,6 @@ class DBObjectSearch extends DBSearch
 	protected function MakeSQLObjectQuerySingleTable(&$oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues)
 	{
 		// $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields))
-//echo "MakeSQLObjectQuery($sTableClass)-liste des clefs externes($sTableClass): <pre>".print_r($aExtKeys, true)."</pre><br/>\n";
 
 		// Prepare the query for a single table (compound objects)
 		// Ignores the items (attributes/filters) that are not on the target table
@@ -1953,7 +1949,6 @@ class DBObjectSearch extends DBSearch
 			}
 			else
 			{
-//echo "<p>MakeSQLObjectQuerySingleTable: Field $sAttCode is part of the table $sTable (named: $sTableAlias)</p>";
 				// standard field, or external key
 				// add it to the output
 				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
@@ -1971,7 +1966,6 @@ class DBObjectSearch extends DBSearch
 			}
 		}
 
-//echo "MakeSQLObjectQuery- Classe $sTableClass<br/>\n";
 		// 4 - The external keys -> joins...
 		//
 		$aAllPointingTo = $this->GetCriteria_PointingTo();
@@ -1983,10 +1977,8 @@ class DBObjectSearch extends DBSearch
 				$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
 
 				$aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode);
-//echo "MakeSQLObjectQuery-Cle '$sKeyAttCode'<br/>\n";
 				if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
 				{
-//echo "MakeSQLObjectQuery-Ajoutons l'operateur TREE_OPERATOR_EQUALS pour $sKeyAttCode<br/>\n";
 					// The join was not explicitely defined in the filter,
 					// we need to do it now
 					$sKeyClass =  $oKeyAttDef->GetTargetClass();
@@ -1997,8 +1989,7 @@ class DBObjectSearch extends DBSearch
 				}
 			}
 		}
-//echo "MakeSQLObjectQuery-liste des clefs de jointure: <pre>".print_r(array_keys($aAllPointingTo), true)."</pre><br/>\n";
-				
+
 		foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
 		{
 			foreach($aPointingTo as $iOperatorCode => $aFilter)
@@ -2011,11 +2002,8 @@ class DBObjectSearch extends DBSearch
 					$sKeyClass =  $oExtFilter->GetFirstJoinedClass();
 					$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
 
-//echo "MakeSQLObjectQuery-$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
 
-//echo "MakeSQLObjectQuery-array_key_exists($sTableClass, \$aExtKeys)<br/>\n";
 					if ($iOperatorCode == TREE_OPERATOR_EQUALS)
 					{
 						if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
@@ -2025,12 +2013,10 @@ class DBObjectSearch extends DBSearch
 							$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
 							foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
 							{
-//echo "MakeSQLObjectQuery aExtKeys[$sTableClass][$sKeyAttCode] => $sAttCode-oAtt: <pre>".print_r($oAtt, true)."</pre><br/>\n";
-								if ($oAtt instanceof AttributeFriendlyName)
+								if ($oAtt->IsFriendlyName())
 								{
 									// 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
 								{
@@ -2040,9 +2026,7 @@ class DBObjectSearch extends DBSearch
 									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";
 								}
 							}
 
@@ -2064,15 +2048,11 @@ class DBObjectSearch extends DBSearch
 
 							// Translate prior to recursing
 							//
-//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."\n".print_r($aTranslateNow, true)."</pre></p>\n";
 							$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
-//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
-		
-//echo "<p>External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()/p>\n";
+
 							self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()");
 							$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
 			
-//echo "<p>Recursive MakeSQLObjectQuery ".__LINE__.": <pre>\n".print_r($oBuild->GetRootFilter()->GetSelectedClasses(), true)."</pre></p>\n";
 							$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
 			
 							$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
@@ -2114,9 +2094,7 @@ class DBObjectSearch extends DBSearch
 
 		// Translate the selected columns
 		//
-//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
 		$oBuild->m_oQBExpressions->Translate($aTranslation, false);
-//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
 
 		// Filter out archived records
 		//
@@ -2125,7 +2103,6 @@ class DBObjectSearch extends DBSearch
 			if (!$oBuild->GetRootFilter()->GetArchiveMode())
 			{
 				$bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses());
-				//$bIsOnJoinedClass = true;
 				if ($bIsOnJoinedClass)
 				{
 					if (MetaModel::IsParentClass($sTableClass, $sTargetClass))
@@ -2136,7 +2113,84 @@ class DBObjectSearch extends DBSearch
 				}
 			}
 		}
-		//MyHelpers::var_dump_html($oSelectBase->RenderSelect());
 		return $oSelectBase;
 	}
+
+	/**
+	 *	Get the expression for the class and its subclasses (if finalclass = 'subclass' ...)
+	 *	Simplifies the final expression by grouping classes having the same expression
+	 */
+	static protected function GetPolymorphicExpression($sClass, $sAttCode)
+	{
+		// 1st step - get all of the required expressions (instantiable classes)
+		//            and group them using their OQL representation
+		//
+		$aExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes)
+		foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass)
+		{
+			if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue;
+
+			$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
+			$oSubClassExp = $oAttDef->GetOQLExpression($sSubClass);
+
+			// 3rd step - position the attributes in the hierarchy of classes
+			//
+			$oSubClassExp->Browse(function($oNode) use ($sSubClass) {
+				if ($oNode instanceof FieldExpression)
+				{
+					$sAttCode = $oNode->GetName();
+					$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
+					if ($oAttDef->IsExternalField())
+					{
+						$sKeyAttCode = $oAttDef->GetKeyAttCode();
+						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
+					}
+					else
+					{
+						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
+					}
+					$sParent = MetaModel::GetAttributeOrigin($sClassOfAttribute, $oNode->GetName());
+					$oNode->SetParent($sParent);
+				}
+			});
+
+			$sSignature = $oSubClassExp->Render();
+			if (!array_key_exists($sSignature, $aExpressions))
+			{
+				$aExpressions[$sSignature] = array(
+					'expression' => $oSubClassExp,
+					'classes' => array(),
+				);
+			}
+			$aExpressions[$sSignature]['classes'][] = $sSubClass;
+		}
+
+		// 2nd step - build the final name expression depending on the finalclass
+		//
+		if (count($aExpressions) == 1)
+		{
+			$aExpData = reset($aExpressions);
+			$oExpression = $aExpData['expression'];
+		}
+		else
+		{
+			$oExpression = null;
+			foreach ($aExpressions as $sSignature => $aExpData)
+			{
+				$oClassListExpr = ListExpression::FromScalars($aExpData['classes']);
+				$oClassExpr = new FieldExpression('finalclass', $sClass);
+				$oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
+
+				if (is_null($oExpression))
+				{
+					$oExpression = $aExpData['expression'];
+				}
+				else
+				{
+					$oExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oExpression));
+				}
+			}
+		}
+		return $oExpression;
+	}
 }

+ 99 - 32
core/metamodel.class.php

@@ -324,6 +324,18 @@ abstract class MetaModel
 		self::_check_subclass($sClass);
 		return self::$m_aClassParams[$sClass]["archive"];
 	}
+	final static public function IsObsoletable($sClass)
+	{
+		self::_check_subclass($sClass);
+		return (!is_null(self::$m_aClassParams[$sClass]['obsolescence_expression']));
+	}
+	final static public function GetObsolescenceExpression($sClass)
+	{
+		self::_check_subclass($sClass);
+		$sOql = self::$m_aClassParams[$sClass]['obsolescence_expression'];
+		$oRet = Expression::FromOQL($sOql);
+		return $oRet;
+	}
 	final static public function GetNameSpec($sClass)
 	{
 		self::_check_subclass($sClass);
@@ -379,20 +391,10 @@ abstract class MetaModel
 				//
 				$iReplacement = (int)$sPiece - 1;
 
-		      if (isset($aAttributes[$iReplacement]))
-		      {
-		      	$sAttCode = $aAttributes[$iReplacement];
-					$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
-					if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
-					{
-						$sKeyAttCode = $oAttDef->GetKeyAttCode();
-						$sClassOfAttribute = self::GetAttributeOrigin($sClass, $sKeyAttCode);
-					}
-					else
-					{
-			      	$sClassOfAttribute = self::GetAttributeOrigin($sClass, $sAttCode);
-			      }
-					$aExpressions[] = new FieldExpression($sAttCode, $sClassOfAttribute);
+				if (isset($aAttributes[$iReplacement]))
+				{
+					$sAttCode = $aAttributes[$iReplacement];
+					$aExpressions[] = new FieldExpression($sAttCode);
 				}
 			}
 			else
@@ -1758,6 +1760,31 @@ abstract class MetaModel
 							self::$m_aClassParams[$sPHPClass]['archive_root_class'] = $bArchiveRoot ? $sPHPClass : self::$m_aClassParams[$sParent]['archive_root_class'];
 						}
 
+						// Inherit obsolescence expression
+						$sObsolescence = null;
+						if (isset(self::$m_aClassParams[$sPHPClass]['obsolescence_expression']))
+						{
+							// Defined or overloaded
+							$sObsolescence = self::$m_aClassParams[$sPHPClass]['obsolescence_expression'];
+						}
+						elseif (@self::$m_aClassParams[$sParent]['obsolescence_expression'])
+						{
+							// Inherited
+							$sObsolescence = self::$m_aClassParams[$sParent]['obsolescence_expression'];
+						}
+						self::$m_aClassParams[$sPHPClass]['obsolescence_expression'] = $sObsolescence;
+
+						if (@self::$m_aClassParams[$sParent]['obsolescence_expression'])
+						{
+							// Inherited or overloaded
+							self::$m_aClassParams[$sPHPClass]['obsolescence_root_class'] = self::$m_aClassParams[$sParent]['obsolescence_root_class'];
+						}
+						elseif ($sObsolescence)
+						{
+							// Defined
+							self::$m_aClassParams[$sPHPClass]['obsolescence_root_class'] = $sPHPClass;
+						}
+
 						foreach (MetaModel::EnumPlugins('iOnClassInitialization') as $sPluginClass => $oClassInit)
 						{
 							$oClassInit->OnAfterClassInitialization($sPHPClass);
@@ -1814,7 +1841,7 @@ abstract class MetaModel
 		{
 			// Create the friendly name attribute
 			$sFriendlyNameAttCode = 'friendlyname'; 
-			$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode, 'id');
+			$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode);
 			self::AddMagicAttribute($oFriendlyName, $sClass);
 
 			if (self::$m_aClassParams[$sClass]["archive_root"])
@@ -1831,14 +1858,33 @@ abstract class MetaModel
 				$sArchiveRoot = self::$m_aClassParams[$sClass]['archive_root_class'];
 				// Inherit archive attributes
 				$oArchiveFlag = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_flag'];
-				$oArchiveFlag->SetHostClass($sArchiveRoot);
+				$oArchiveFlag->SetHostClass($sClass);
 				self::$m_aAttribDefs[$sClass]['archive_flag'] = $oArchiveFlag;
 				self::$m_aAttribOrigins[$sClass]['archive_flag'] = $sArchiveRoot;
 				$oArchiveDate = clone self::$m_aAttribDefs[$sArchiveRoot]['archive_date'];
-				$oArchiveDate->SetHostClass($sArchiveRoot);
+				$oArchiveDate->SetHostClass($sClass);
 				self::$m_aAttribDefs[$sClass]['archive_date'] = $oArchiveDate;
 				self::$m_aAttribOrigins[$sClass]['archive_date'] = $sArchiveRoot;
 			}
+			if (!is_null(self::$m_aClassParams[$sClass]['obsolescence_expression']))
+			{
+				$oObsolescenceFlag = new AttributeObsolescenceFlag('obsolescence_flag');
+				self::AddMagicAttribute($oObsolescenceFlag, $sClass);
+
+				$sObsolescenceRoot = self::$m_aClassParams[$sClass]['obsolescence_root_class'];
+				if ($sClass == $sObsolescenceRoot)
+				{
+					$oObsolescenceDate = new AttributeDate('obsolescence_date', array('magic' => true, "allowed_values" => null, "sql" => 'obsolescence_date', "default_value" => '', "is_null_allowed" => true, "depends_on" => array()));
+					self::AddMagicAttribute($oObsolescenceDate, $sClass);
+				}
+				else
+				{
+					$oObsolescenceDate = clone self::$m_aAttribDefs[$sObsolescenceRoot]['archive_date'];
+					$oObsolescenceDate->SetHostClass($sClass);
+					self::$m_aAttribDefs[$sClass]['obsolescence_date'] = $oObsolescenceDate;
+					self::$m_aAttribOrigins[$sClass]['obsolescence_date'] = $sObsolescenceRoot;
+				}
+			}
 			foreach (self::$m_aAttribDefs[$sClass] as $sAttCode => $oAttDef)
 			{
 				// Compute the filter codes
@@ -1882,8 +1928,8 @@ abstract class MetaModel
 					else
 					{
 						// Create the friendly name attribute
-						$sFriendlyNameAttCode = $sAttCode.'_friendlyname'; 
-						$oFriendlyName = new AttributeFriendlyName($sFriendlyNameAttCode, $sAttCode);
+						$sFriendlyNameAttCode = $sAttCode.'_friendlyname';
+						$oFriendlyName = new AttributeExternalField($sFriendlyNameAttCode, array('magic' => true, 'allowed_values'=>null, 'extkey_attcode'=>$sAttCode, "target_attcode"=>'friendlyname', 'depends_on'=>array()));
 						self::AddMagicAttribute($oFriendlyName, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
 
 						if (self::HasChildrenClasses($sRemoteClass))
@@ -1936,6 +1982,12 @@ abstract class MetaModel
 						$oArchiveRemote = new AttributeExternalField($sArchiveRemote, array("allowed_values"=>null, "extkey_attcode"=>$sAttCode, "target_attcode"=>'archive_flag', "depends_on"=>array()));
 						self::AddMagicAttribute($oArchiveRemote, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
 					}
+					if (self::IsObsoletable($sRemoteClass))
+					{
+						$sObsoleteRemote = $sAttCode.'_obsolescence_flag';
+						$oObsoleteRemote = new AttributeExternalField($sObsoleteRemote, array("allowed_values"=>null, "extkey_attcode"=>$sAttCode, "target_attcode"=>'archive_flag', "depends_on"=>array()));
+						self::AddMagicAttribute($oObsoleteRemote, $sClass, self::$m_aAttribOrigins[$sClass][$sAttCode]);
+					}
 				}
 				if ($oAttDef instanceof AttributeMetaEnum)
 				{
@@ -2490,6 +2542,18 @@ abstract class MetaModel
 		}
 		return $aRes;
 	}
+	public static function EnumObsoletableClasses()
+	{
+		$aRes = array();
+		foreach (self::GetClasses() as $sClass)
+		{
+			if (self::IsObsoletable($sClass))
+			{
+				$aRes[] = $sClass;
+			}
+		}
+		return $aRes;
+	}
 
 	public static function HasChildrenClasses($sClass)
 	{
@@ -5157,26 +5221,29 @@ abstract class MetaModel
 		else
 		{
 			$oAttDef = static::GetAttributeDef($sClass, $sField);
-			if ($oAttDef instanceof AttributeFriendlyName)
+			if ($oAttDef->IsExternalField())
 			{
-				$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $oAttDef->GetKeyAttCode());
-				$sRemoteClass = $oKeyAttDef->GetTargetClass();
-				$sFriendlyNameAttCode = static::GetFriendlyNameAttributeCode($sRemoteClass);
-				if (is_null($sFriendlyNameAttCode))
+				if ($oAttDef->IsFriendlyName())
 				{
-					// The friendly name is made of several attributes
-					$sRet = $oAttDef->GetKeyAttCode().'->friendlyname';
+					$oKeyAttDef = MetaModel::GetAttributeDef($sClass, $oAttDef->GetKeyAttCode());
+					$sRemoteClass = $oKeyAttDef->GetTargetClass();
+					$sFriendlyNameAttCode = static::GetFriendlyNameAttributeCode($sRemoteClass);
+					if (is_null($sFriendlyNameAttCode))
+					{
+						// The friendly name is made of several attributes
+						$sRet = $oAttDef->GetKeyAttCode().'->friendlyname';
+					}
+					else
+					{
+						// The friendly name is made of a single attribute
+						$sRet = $oAttDef->GetKeyAttCode().'->'.$sFriendlyNameAttCode;
+					}
 				}
 				else
 				{
-					// The friendly name is made of a single attribute
-					$sRet = $oAttDef->GetKeyAttCode().'->'.$sFriendlyNameAttCode;
+					$sRet = $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode();
 				}
 			}
-			elseif ($oAttDef->IsExternalField())
-			{
-				$sRet = $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode();
-			}
 		}
 		return $sRet;
 	}

+ 79 - 3
core/oql/expression.class.inc.php

@@ -45,6 +45,13 @@ abstract class Expression
 	// recursive rendering (aArgs used as input by default, or used as output if bRetrofitParams set to True
 	abstract public function Render(&$aArgs = null, $bRetrofitParams = false);
 
+	/**
+	 * Recursively browse the expression tree
+	 * @param Closure $callback
+	 * @return mixed
+	 */
+	abstract public function Browse(Closure $callback);
+
 	abstract public function ApplyParameters($aArgs);
 	
 	// recursively builds an array of class => fieldname
@@ -76,6 +83,10 @@ abstract class Expression
 		return self::FromOQL(base64_decode($sValue));
 	}
 
+	/**
+	 * @param $sConditionExpr
+	 * @return Expression
+	 */
 	static public function FromOQL($sConditionExpr)
 	{
 		$oOql = new OqlInterpreter($sConditionExpr);
@@ -139,6 +150,11 @@ class SQLExpression extends Expression
 		return $this->m_sSQL;
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 	}
@@ -245,7 +261,14 @@ class BinaryExpression extends Expression
 		$sRight = $this->GetRightExpr()->Render($aArgs, $bRetrofitParams);
 		return "($sLeft $sOperator $sRight)";
 	}
-	
+
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+		$this->m_oLeftExpr->Browse($callback);
+		$this->m_oRightExpr->Browse($callback);
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 		if ($this->m_oLeftExpr instanceof VariableExpression)
@@ -358,7 +381,7 @@ class UnaryExpression extends Expression
 	public function GetValue()
 	{
 		return $this->m_value;
-	} 
+	}
 
 	// recursive rendering
 	public function Render(&$aArgs = null, $bRetrofitParams = false)
@@ -366,6 +389,11 @@ class UnaryExpression extends Expression
 		return CMDBSource::Quote($this->m_value);
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 	}
@@ -484,6 +512,12 @@ class FieldExpression extends UnaryExpression
 	public function GetParent() {return $this->m_sParent;}
 	public function GetName() {return $this->m_sName;}
 
+	public function SetParent($sParent)
+	{
+		$this->m_sParent = $sParent;
+		$this->m_value = $sParent.'.'.$this->m_sName;
+	}
+
 	// recursive rendering
 	public function Render(&$aArgs = null, $bRetrofitParams = false)
 	{
@@ -680,7 +714,7 @@ class VariableExpression extends UnaryExpression
 			throw new MissingQueryArgument('Missing query argument', array('expecting'=>$this->m_sName, 'available'=>array_keys($aArgs)));
 		}
 	}
-	
+
 	public function RenameParam($sOldName, $sNewName)
 	{
 		if ($this->m_sName == $sOldName)
@@ -768,6 +802,15 @@ class ListExpression extends Expression
 		return '('.implode(', ', $aRes).')';
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+		foreach ($this->m_aExpressions as $oExpr)
+		{
+			$oExpr->Browse($callback);
+		}
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 		$aRes = array();
@@ -888,6 +931,15 @@ class FunctionExpression extends Expression
 		return $this->m_sVerb.'('.implode(', ', $aRes).')';
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+		foreach ($this->m_aArgs as $iPos => $oExpr)
+		{
+			$oExpr->Browse($callback);
+		}
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 		$aRes = array();
@@ -1071,6 +1123,12 @@ class IntervalExpression extends Expression
 		return 'INTERVAL '.$this->m_oValue->Render($aArgs, $bRetrofitParams).' '.$this->m_sUnit;
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+		$this->m_oValue->Browse($callback);
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 		if ($this->m_oValue instanceof VariableExpression)
@@ -1151,6 +1209,15 @@ class CharConcatExpression extends Expression
 		return "CAST(CONCAT(".implode(', ', $aRes).") AS CHAR)";
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+		foreach ($this->m_aExpressions as $oExpr)
+		{
+			$oExpr->Browse($callback);
+		}
+	}
+
 	public function ApplyParameters($aArgs)
 	{
 		$aRes = array();
@@ -1255,6 +1322,15 @@ class CharConcatWSExpression extends CharConcatExpression
 		return "CAST(CONCAT_WS($sSep, ".implode(', ', $aRes).") AS CHAR)";
 	}
 
+	public function Browse(Closure $callback)
+	{
+		$callback($this);
+		foreach ($this->m_aExpressions as $oExpr)
+		{
+			$oExpr->Browse($callback);
+		}
+	}
+
 	public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
 	{
 		$aRes = array();

+ 8 - 4
core/oql/oqlinterpreter.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2017 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -20,7 +20,7 @@
 /**
  * Wrapper to execute the parser, lexical analyzer and normalization of an OQL query
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2017 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -83,6 +83,9 @@ class OqlInterpreter
 		return $res;
 	}
 
+	/**
+	 * @return OqlQuery
+	 */
 	public function ParseQuery()
 	{
 		$oRes = $this->Parse();
@@ -93,6 +96,9 @@ class OqlInterpreter
 		return $oRes;
 	}
 
+	/**
+	 * @return Expression
+	 */
 	public function ParseExpression()
 	{
 		$oRes = $this->Parse();
@@ -103,5 +109,3 @@ class OqlInterpreter
 		return $oRes;
 	}
 }
-
-?>

+ 1 - 0
dictionaries/dictionary.itop.ui.php

@@ -775,6 +775,7 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Tag:Archived+' => 'Can be accessed only in archive mode',
 	'Tag:Synchronized' => 'Synchronized',
 	'ObjectRef:Archived' => 'Archived',
+	'ObjectRef:Obsolete' => 'Obsolete',
 	'UI:SearchResultsPageTitle' => 'iTop - Search Results',
 	'UI:SearchResultsTitle' => 'Search Results',
 	'UI:SearchResultsTitle+' => 'Full-text search results',

+ 8 - 1
setup/compiler.class.inc.php

@@ -994,6 +994,14 @@ EOF
 			$bEnabled = $this->GetPropBoolean($oArchive, 'enabled', false);
 			$aClassParams['archive'] = $bEnabled;
 		}
+		if ($oObsolescence = $oProperties->GetOptionalElement('obsolescence'))
+		{
+			$sCondition = trim($this->GetPropString($oObsolescence, 'condition', ''));
+			if ($sCondition != "''")
+			{
+				$aClassParams['obsolescence_expression'] = $sCondition;
+			}
+		}
 
 		// Finalize class params declaration
 		//
@@ -1687,7 +1695,6 @@ EOF
 			$sMethods .= "\t}\n";
 		}
 
-
 		// Let's make the whole class declaration
 		//
 		$sPHP = "\n\n$sCodeComment\n";

+ 2 - 6
webservices/export.php

@@ -162,13 +162,9 @@ if (!empty($sExpression))
 			{
 				$aAliasToFields[$sClassAlias][] = $oAttDef->GetParentAttCode();
 			}
-			else if($oAttDef instanceof AttributeFriendlyname)
+			else if($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName())
 			{
-				$sKeyAttCode = $oAttDef->GetKeyAttCode();
-				if ($sKeyAttCode != 'id')
-				{
-					$aAliasToFields[$sClassAlias][] = $sKeyAttCode;
-				}
+				$aAliasToFields[$sClassAlias][] = $sKeyAttCode;
 			}
 		}