浏览代码

Internal:
- code refactoring to generalize attributes based on an OQL expression (friendly name, obsolescence flag, ....). The intermediate class AttributeComputedFieldVoid has been swept in favor of the use of a new method: IsBasedOnOQLExpresssion.
- added an introspection API (experimental), allowing an external application to request for information about the capabilities of the framework (first step: list attributes and their main characteristics)

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

romainq 8 年之前
父节点
当前提交
3109a0a7ca
共有 5 个文件被更改,包括 352 次插入244 次删除
  1. 204 137
      core/attributedef.class.inc.php
  2. 1 1
      core/dbobject.class.php
  3. 53 105
      core/dbobjectsearch.class.php
  4. 93 0
      core/introspection.class.inc.php
  5. 1 1
      core/metamodel.class.php

+ 204 - 137
core/attributedef.class.inc.php

@@ -226,20 +226,93 @@ abstract class AttributeDefinition
 	{
 		return $this;
 	}
-	public function IsDirectField() {return false;} 
-	public function IsScalar() {return false;} 
-	public function IsLinkSet() {return false;} 
-	public function IsExternalKey($iType = EXTKEY_RELATIVE) {return false;} 
-	public function IsHierarchicalKey() {return false;}
-	public function IsExternalField() {return false;} 
+
+	/**
+	 * Deprecated - use IsBasedOnDBColumns instead
+	 * @return bool
+	 */
+	public function IsDirectField() {return static::IsBasedOnDBColumns();}
+
+	/**
+	 * Returns true if the attribute value is built after DB columns
+	 * @return bool
+	 */
+	static public function IsBasedOnDBColumns() {return false;}
+	/**
+	 * Returns true if the attribute value is built after other attributes by the mean of an expression
+	 * @return bool
+	 */
+	static public function IsBasedOnOQLExpression() {return false;}
+	/**
+	 * Returns true if the attribute value can be shown as a string
+	 * @return bool
+	 */
+	static public function IsScalar() {return false;}
+	/**
+	 * Returns true if the attribute value is a set of related objects (1-N or N-N)
+	 * @return bool
+	 */
+	static public function IsLinkSet() {return false;}
+	/**
+	 * Returns true if the attribute is an external key, either directly (RELATIVE to the host class), or indirectly (ABSOLUTELY)
+	 * @return bool
+	 */
+	public function IsExternalKey($iType = EXTKEY_RELATIVE) {return false;}
+	/**
+	 * Returns true if the attribute value is an external key, pointing to the host class
+	 * @return bool
+	 */
+	static public function IsHierarchicalKey() {return false;}
+	/**
+	 * Returns true if the attribute value is stored on an object pointed to be an external key
+	 * @return bool
+	 */
+	static public function IsExternalField() {return false;}
+	/**
+	 * Returns true if the attribute can be written (by essence)
+	 * @return bool
+	 */
 	public function IsWritable() {return false;}
+	/**
+	 * Returns true if the attribute has been added automatically by the framework
+	 * @return bool
+	 */
 	public function IsMagic() {return $this->GetOptional('magic', false);}
-	public function LoadInObject() {return true;}
-	public function LoadFromDB() {return true;}
+	/**
+	 * Returns true if the attribute value is kept in the loaded object (in memory)
+	 * @return bool
+	 */
+	static public function LoadInObject() {return true;}
+	/**
+	 * Returns true if the attribute value comes from the database in one way or another
+	 * @return bool
+	 */
+	static public function LoadFromDB() {return true;}
+	/**
+	 * Returns true if the attribute should be loaded anytime (in addition to the column selected by the user)
+	 * @return bool
+	 */
 	public function AlwaysLoadInTables() {return $this->GetOptional('always_load_in_tables', false);}
-	public function GetValue($oHostObject){return null;} // must return the value if LoadInObject returns false
-	public function IsNullAllowed() {return true;} 
-	public function GetCode() {return $this->m_sCode;} 
+	/**
+	 * Must return the value if LoadInObject returns false
+	 * @return mixed
+	 */
+	public function GetValue($oHostObject){return null;}
+	/**
+	 * Returns true if the attribute must not be stored if its current value is "null" (Cf. IsNull())
+	 * @return bool
+	 */
+	public function IsNullAllowed() {return true;}
+	/**
+	 * Returns the attribute code (identifies the attribute in the host class)
+	 * @return string
+	 */
+	public function GetCode() {return $this->m_sCode;}
+
+	/**
+	 * Find the corresponding "link" attribute on the target class, if any
+	 * @return null | AttributeDefinition
+	 */
 	public function GetMirrorLinkAttribute() {return null;}
 
 	/**
@@ -774,8 +847,8 @@ class AttributeLinkedSet extends AttributeDefinition
 
 	public function GetEditClass() {return "LinkedSet";}
 
-	public function IsWritable() {return true;} 
-	public function IsLinkSet() {return true;} 
+	public function IsWritable() {return true;}
+	static public function IsLinkSet() {return true;}
 	public function IsIndirect() {return false;} 
 
 	public function GetValuesDef() {return $this->Get("allowed_values");} 
@@ -899,7 +972,7 @@ class AttributeLinkedSet extends AttributeDefinition
 					}
 					if ($sAttCode == $this->GetExtKeyToMe()) continue;
 					if ($oAttDef->IsExternalField()) continue;
-					if (!$oAttDef->IsDirectField()) continue;
+					if (!$oAttDef->IsBasedOnDBColumns()) continue;
 					if (!$oAttDef->IsScalar()) continue;
 					$sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize);
 					if (strlen($sAttValue) > 0)
@@ -1162,7 +1235,7 @@ class AttributeLinkedSet extends AttributeDefinition
 					}
 					if ($sAttCode == $this->GetExtKeyToMe()) continue;
 					if ($oAttDef->IsExternalField()) continue;
-					if (!$oAttDef->IsDirectField()) continue;
+					if (!$oAttDef->IsBasedOnDBColumns()) continue;
 					if (!$oAttDef->IsScalar()) continue;
 					$attValue = $oObj->Get($sAttCode);
 					$aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue);
@@ -1265,9 +1338,8 @@ class AttributeLinkedSet extends AttributeDefinition
 	}
 
 	/**
-	 * Find the corresponding "link" attribute on the target class
-	 * 	 
-	 * @return string The attribute code on the target class, or null if none has been found
+	 * Find the corresponding "link" attribute on the target class, if any
+	 * @return null | AttributeDefinition
 	 */
 	public function GetMirrorLinkAttribute()
 	{
@@ -1349,9 +1421,8 @@ class AttributeLinkedSetIndirect extends AttributeLinkedSet
 	}
 
 	/**
-	 * Find the corresponding "link" attribute on the target class
-	 * 	 
-	 * @return string The attribute code on the target class, or null if none has been found
+	 * Find the corresponding "link" attribute on the target class, if any
+	 * @return null | AttributeDefinition
 	 */
 	public function GetMirrorLinkAttribute()
 	{
@@ -1416,8 +1487,8 @@ class AttributeDBFieldVoid extends AttributeDefinition
 	public function GetValuesDef() {return $this->Get("allowed_values");} 
 	public function GetPrerequisiteAttributes($sClass = null) {return $this->Get("depends_on");}
 
-	public function IsDirectField() {return true;} 
-	public function IsScalar() {return true;} 
+	static public function IsBasedOnDBColumns() {return true;}
+	static public function IsScalar() {return true;}
 	public function IsWritable() {return !$this->IsMagic();}
 	public function GetSQLExpr()
 	{
@@ -4508,9 +4579,8 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 	}
 
 	/**
-	 * Find the corresponding "link" attribute on the target class
-	 * 	 
-	 * @return string The attribute code on the target class, or null if none has been found
+	 * Find the corresponding "link" attribute on the target class, if any
+	 * @return null | AttributeDefinition
 	 */
 	public function GetMirrorLinkAttribute()
 	{
@@ -4617,7 +4687,7 @@ class AttributeHierarchicalKey extends AttributeExternalKey
 		parent::SetHostClass($sHostClass);
 	}
 
-	public function IsHierarchicalKey() {return true;}
+	static public function IsHierarchicalKey() {return true;}
 	public function GetTargetClass($iType = EXTKEY_RELATIVE) {return $this->m_sTargetClass;}
 	public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;}
 	public function GetKeyAttCode() {return $this->GetCode();}
@@ -4706,9 +4776,8 @@ class AttributeHierarchicalKey extends AttributeExternalKey
 	}
 
 	/**
-	 * Find the corresponding "link" attribute on the target class
-	 * 	 
-	 * @return string The attribute code on the target class, or null if none has been found
+	 * Find the corresponding "link" attribute on the target class, if any
+	 * @return null | AttributeDefinition
 	 */
 	public function GetMirrorLinkAttribute()
 	{
@@ -4808,7 +4877,7 @@ class AttributeExternalField extends AttributeDefinition
 		return $this->GetKeyAttDef($iType)->GetTargetClass();
 	}
 
-	public function IsExternalField() {return true;} 
+	static public function IsExternalField() {return true;}
 	public function GetKeyAttCode() {return $this->Get("extkey_attcode");} 
 	public function GetExtAttCode() {return $this->Get("target_attcode");} 
 
@@ -4868,11 +4937,10 @@ class AttributeExternalField extends AttributeDefinition
 		return $oExtAttDef->IsNullAllowed(); 
 	}
 
-	public function IsScalar()
+	static public function IsScalar()
 	{
-		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->IsScalar(); 
-	} 
+		return true;
+	}
 
 	public function GetFilterDefinitions()
 	{
@@ -5054,10 +5122,10 @@ class AttributeBlob extends AttributeDefinition
 	}
 
 	public function GetEditClass() {return "Document";}
-	
-	public function IsDirectField() {return true;} 
-	public function IsScalar() {return true;} 
-	public function IsWritable() {return true;} 
+
+	static public function IsBasedOnDBColumns() {return true;}
+	static public function IsScalar() {return true;}
+	public function IsWritable() {return true;}
 	public function GetDefaultValue(DBObject $oHostObject = null) {return "";}
 	public function IsNullAllowed(DBObject $oHostObject = null) {return $this->GetOptional("is_null_allowed", false);}
 
@@ -5412,9 +5480,9 @@ class AttributeStopWatch extends AttributeDefinition
 	}
 
 	public function GetEditClass() {return "StopWatch";}
-	
-	public function IsDirectField() {return true;} 
-	public function IsScalar() {return true;} 
+
+	static public function IsBasedOnDBColumns() {return true;}
+	static public function IsScalar() {return true;}
 	public function IsWritable() {return true;}
 	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->NewStopWatch();}
 
@@ -6092,15 +6160,15 @@ class AttributeSubItem extends AttributeDefinition
 
 	public function GetEditClass() {return "";}
 	
-	public function GetValuesDef() {return null;} 
+	public function GetValuesDef() {return null;}
 
-	public function IsDirectField() {return true;} 
-	public function IsScalar() {return true;} 
-	public function IsWritable() {return false;} 
+	static public function IsBasedOnDBColumns() {return true;}
+	static public function IsScalar() {return true;}
+	public function IsWritable() {return false;}
 	public function GetDefaultValue(DBObject $oHostObject = null) {return null;}
 //	public function IsNullAllowed() {return false;}
 
-	public function LoadInObject() {return false;} // if this verb returns false, then GetValue must be implemented
+	static public function LoadInObject() {return false;} // if this verb returns false, then GetValue must be implemented
 
 	/**
 	 * Used by DBOBject::Get()
@@ -6245,10 +6313,10 @@ class AttributeOneWayPassword extends AttributeDefinition
 	}
 
 	public function GetEditClass() {return "One Way Password";}
-	
-	public function IsDirectField() {return true;} 
-	public function IsScalar() {return true;} 
-	public function IsWritable() {return true;} 
+
+	static public function IsBasedOnDBColumns() {return true;}
+	static public function IsScalar() {return true;}
+	public function IsWritable() {return true;}
 	public function GetDefaultValue(DBObject $oHostObject = null) {return "";}
 	public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);}
 
@@ -6619,105 +6687,105 @@ class AttributePropertySet extends AttributeTable
  *
  * @package	 iTopORM
  */
-class AttributeComputedFieldVoid extends AttributeDefinition
-{	
-	static public function ListExpectedParams()
+
+/**
+ * The attribute dedicated to the friendly name automatic attribute (not written) 
+ *
+ * @package	 iTopORM
+ */
+class AttributeFriendlyName extends AttributeDefinition
+{
+	public function __construct($sCode, $sExtKeyAttCode)
 	{
-		return array_merge(parent::ListExpectedParams(), array());
+		$this->m_sCode = $sCode;
+		$aParams = array();
+		$aParams["default_value"] = '';
+		$aParams["extkey_attcode"] = $sExtKeyAttCode;
+		parent::__construct($sCode, $aParams);
+
+		$this->m_sValue = $this->Get("default_value");
 	}
 
+
 	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;} 
-	public function IsScalar() {return true;} 
-	public function IsWritable() {return false;} 
-	public function GetSQLExpr()
-	{
-		return null;
-	}
+	public function GetValuesDef() {return null;}
+	public function GetPrerequisiteAttributes($sClass = null) {return $this->GetOptional("depends_on", array());}
 
-	public function GetDefaultValue(DBObject $oHostObject = null) {return $this->MakeRealValue("", $oHostObject);}
+	static public function IsScalar() {return true;}
 	public function IsNullAllowed() {return false;}
 
-	// 
-//	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
-
 	public function GetSQLExpressions($sPrefix = '')
 	{
 		if ($sPrefix == '')
 		{
 			$sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property
 		}
-		return array('' => $sPrefix); 
+		return array('' => $sPrefix);
 	}
 
-	public function FromSQLToValue($aCols, $sPrefix = '')
+	static public function IsBasedOnOQLExpression() {return true;}
+	public function GetOQLExpression()
 	{
-		return null;
+		return static::GetExtendedNameExpression($this->GetHostClass());
 	}
-	public function GetSQLValues($value)
-	{
-		return array();
-	}
-
-	public function GetSQLColumns($bFullSpec = false)
-	{
-		return array();
-	}
-
-	public function GetFilterDefinitions()
+	/**
+	 *	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)
 	{
-		return array($this->GetCode() => new FilterFromAttribute($this));
-	}
+		// 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;
 
-	public function GetBasicFilterOperators()
-	{
-		return array("="=>"equals", "!="=>"differs from");
-	}
-	public function GetBasicFilterLooseOperator()
-	{
-		return "=";
-	}
+			$oSubClassName = MetaModel::GetNameExpression($sSubClass);
+			$sSignature = $oSubClassName->Render();
+			if (!array_key_exists($sSignature, $aFNExpressions))
+			{
+				$aFNExpressions[$sSignature] = array(
+					'expression' => $oSubClassName,
+					'classes' => array(),
+				);
+			}
+			$aFNExpressions[$sSignature]['classes'][] = $sSubClass;
+		}
 
-	public function GetBasicFilterSQLExpr($sOpCode, $value)
-	{
-		$sQValue = CMDBSource::Quote($value);
-		switch ($sOpCode)
+		// 2nd step - build the final name expression depending on the finalclass
+		//
+		if (count($aFNExpressions) == 1)
 		{
-		case '!=':
-			return $this->GetSQLExpr()." != $sQValue";
-			break;
-		case '=':
-		default:
-			return $this->GetSQLExpr()." = $sQValue";
+			$aExpData = reset($aFNExpressions);
+			$oNameExpression = $aExpData['expression'];
 		}
-	}
-
-	public function IsPartOfFingerprint() { return false; }
-}
-
-/**
- * The attribute dedicated to the friendly name automatic attribute (not written) 
- *
- * @package	 iTopORM
- */
-class AttributeFriendlyName extends AttributeComputedFieldVoid
-{
-	public function __construct($sCode, $sExtKeyAttCode)
-	{
-		$this->m_sCode = $sCode;
-		$aParams = array();
-//		$aParams["is_null_allowed"] = false,
-		$aParams["default_value"] = '';
-		$aParams["extkey_attcode"] = $sExtKeyAttCode;
-		parent::__construct($sCode, $aParams);
+		else
+		{
+			$oNameExpression = null;
+			foreach ($aFNExpressions as $sSignature => $aExpData)
+			{
+				$oClassListExpr = ListExpression::FromScalars($aExpData['classes']);
+				$oClassExpr = new FieldExpression('finalclass', $sClass);
+				$oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
 
-		$this->m_sValue = $this->Get("default_value");
+				if (is_null($oNameExpression))
+				{
+					$oNameExpression = $aExpData['expression'];
+				}
+				else
+				{
+					$oNameExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oNameExpression));
+				}
+			}
+		}
+		return $oNameExpression;
 	}
 
+
 	public function GetKeyAttCode() {return $this->Get("extkey_attcode");} 
 
 	public function GetExtAttCode() {return 'friendlyname';} 
@@ -6759,23 +6827,12 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 		return $sLabel;
 	} 
 
-	// n/a, the friendly name is made of a complex expression (see GetNameSpec)
-	protected function GetSQLCol($bFullSpec = false) {return "";}	
-
 	public function FromSQLToValue($aCols, $sPrefix = '')
 	{
  		$sValue = $aCols[$sPrefix];
 		return $sValue;
 	}
 
-	/**
-	 * Encrypt the value before storing it in the database
-	 */
-	public function GetSQLValues($value)
-	{
-		return array();
-	}
-
 	public function IsWritable()
 	{
 		return false;
@@ -6785,7 +6842,7 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 		return true;
 	}
 
-	public function IsDirectField()
+	static public function IsBasedOnDBColumns()
 	{
 		return false;
 	}
@@ -6836,6 +6893,16 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid
 		return '';
 	}
 
+	public function GetFilterDefinitions()
+	{
+		return array($this->GetCode() => new FilterFromAttribute($this));
+	}
+
+	public function GetBasicFilterOperators()
+	{
+		return array("="=>"equals", "!="=>"differs from");
+	}
+
 	public function GetBasicFilterLooseOperator()
 	{
 		return "Contains";
@@ -7268,7 +7335,7 @@ class AttributeCustomFields extends AttributeDefinition
 
 	public function GetEditClass() {return "CustomFields";}
 	public function IsWritable() {return true;}
-	public function LoadFromDB() {return false;} // See ReadValue...
+	static public function LoadFromDB() {return false;} // See ReadValue...
 
 	public function GetDefaultValue(DBObject $oHostObject = null)
 	{

+ 1 - 1
core/dbobject.class.php

@@ -1999,7 +1999,7 @@ abstract class DBObject implements iDisplay
 			{
 				$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
 				if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true;
-				if ($oAttDef->IsDirectField())
+				if ($oAttDef->IsBasedOnDBColumns())
 				{
 					$aDBChanges[$sAttCode] = $aChanges[$sAttCode];
 				}

+ 53 - 105
core/dbobjectsearch.class.php

@@ -1677,66 +1677,71 @@ class DBObjectSearch extends DBSearch
 		}
 
 		$aFNJoinAlias = array(); // array of (subclass => alias)
-		if (array_key_exists('friendlyname', $aExpectedAtts))
+		foreach ($aExpectedAtts as $sAttCode => $oExpression)
 		{
-			// To optimize: detect a restriction on child classes in the condition expression
-			//    e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine')
-			$oNameExpression = self::GetExtendedNameExpression($sClass);
-
-			$aNameFields = array();
-			$oNameExpression->GetUnresolvedFields('', $aNameFields);
-			$aTranslateNameFields = array();
-			foreach($aNameFields as $sSubClass => $aFields)
+			if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) continue;
+			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+			if ($oAttDef->IsBasedOnOQLExpression())
 			{
-				foreach($aFields as $sAttCode => $oField)
+				// 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);
+
+				$aRequiredFields = array();
+				$oExpression->GetUnresolvedFields('', $aRequiredFields);
+				$aTranslateFields = array();
+				foreach($aRequiredFields as $sSubClass => $aFields)
 				{
-					$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
-					if ($oAttDef->IsExternalKey())
-					{
-						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
-						$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
-					}				
-					elseif ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
-					{
-						$sKeyAttCode = $oAttDef->GetKeyAttCode();
-						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
-						$aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef;
-					}
-					else
+					foreach($aFields as $sAttCode => $oField)
 					{
-						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
-					}
+						$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
+						if ($oAttDef->IsExternalKey())
+						{
+							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
+							$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
+						}
+						elseif ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+						{
+							$sKeyAttCode = $oAttDef->GetKeyAttCode();
+							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
+							$aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef;
+						}
+						else
+						{
+							$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
+						}
 
-					if (MetaModel::IsParentClass($sClassOfAttribute, $sClass))
-					{
-						// The attribute is part of the standard query
-						//
-						$sAliasForAttribute = $sClassAlias;
-					}
-					else
-					{
-						// The attribute will be available from an additional outer join
-						// For each subclass (table) one single join is enough
-						//
-						if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias))
+						if (MetaModel::IsParentClass($sClassOfAttribute, $sClass))
 						{
-							$sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute);
-							$aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute;
+							// The attribute is part of the standard query
+							//
+							$sAliasForAttribute = $sClassAlias;
 						}
 						else
 						{
-							$sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute];
+							// The attribute will be available from an additional outer join
+							// For each subclass (table) one single join is enough
+							//
+							if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias))
+							{
+								$sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute);
+								$aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute;
+							}
+							else
+							{
+								$sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute];
+							}
 						}
-					}
 
-					$aTranslateNameFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute);
+						$aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute);
+					}
 				}
-			}
-			$oNameExpression = $oNameExpression->Translate($aTranslateNameFields, false);
+				$oExpression = $oExpression->Translate($aTranslateFields, false);
 
-			$aTranslateNow = array();
-			$aTranslateNow[$sClassAlias]['friendlyname'] = $oNameExpression;
-			$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
+				$aTranslateNow = array();
+				$aTranslateNow[$sClassAlias]['friendlyname'] = $oExpression;
+				$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
+			}
 		}
 
 		// Add the ext fields used in the select (eventually adds an external key)
@@ -1921,7 +1926,7 @@ class DBObjectSearch extends DBSearch
 			//
 			if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues))
 			{
-				assert ($oAttDef->IsDirectField());
+				assert ($oAttDef->IsBasedOnDBColumns());
 				foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue)
 				{
 					$aUpdateValues[$sColumn] = $sValue;
@@ -2134,61 +2139,4 @@ class DBObjectSearch extends DBSearch
 		//MyHelpers::var_dump_html($oSelectBase->RenderSelect());
 		return $oSelectBase;
 	}
-
-	/**
-	 *	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;
-	}
-
 }

+ 93 - 0
core/introspection.class.inc.php

@@ -0,0 +1,93 @@
+<?php
+// Copyright (C) 2017 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Usage:
+ * require_once(...'introspection.class.inc.php');
+ */
+
+require_once('attributedef.class.inc.php');
+
+class Introspection
+{
+	protected $aAttributeHierarchy = array(); // class => child classes
+	protected $aAttributes = array();
+
+	public function __construct()
+	{
+		$this->InitAttributes();
+	}
+
+	protected function InitAttributes()
+	{
+		foreach(get_declared_classes() as $sPHPClass)
+		{
+			$oRefClass = new ReflectionClass($sPHPClass);
+			if ($sPHPClass == 'AttributeDefinition' || $oRefClass->isSubclassOf('AttributeDefinition'))
+			{
+				if ($oParentClass = $oRefClass->getParentClass())
+				{
+					$sParentClass = $oParentClass->getName();
+					if (!array_key_exists($sParentClass, $this->aAttributeHierarchy))
+					{
+						$this->aAttributeHierarchy[$sParentClass] = array();
+					}
+					$this->aAttributeHierarchy[$sParentClass][] = $sPHPClass;
+				}
+				else
+				{
+					$sParentClass = null;
+				}
+				$this->aAttributes[$sPHPClass] = array(
+					'parent' => $sParentClass,
+					'LoadInObject' => $sPHPClass::LoadInObject(),
+					'LoadFromDB' => $sPHPClass::LoadFromDB(),
+					'IsBasedOnDBColumns' => $sPHPClass::IsBasedOnDBColumns(),
+					'IsBasedOnOQLExpression' => $sPHPClass::IsBasedOnOQLExpression(),
+					'IsExternalField' => $sPHPClass::IsExternalField(),
+					'IsScalar' => $sPHPClass::IsScalar(),
+					'IsLinkset' => $sPHPClass::IsLinkset(),
+					'IsHierarchicalKey' => $sPHPClass::IsHierarchicalKey(),
+				);
+			}
+		}
+	}
+	public function GetAttributes()
+	{
+		return $this->aAttributes;
+	}
+	public function GetAttributeHierarchy()
+	{
+		return $this->aAttributeHierarchy;
+	}
+	public function EnumAttributeCharacteristics()
+	{
+		return array(
+			'LoadInObject' => 'Is the value stored in the object itself?',
+			'LoadFromDB' => 'Is the value read from the DB?',
+			'IsBasedOnDBColumns' => 'Is this a value stored within one or several columns?',
+			'IsBasedOnOQLExpression' => 'Is this a value computed after other attributes, by the mean of an OQL expression?',
+			'IsExternalField' => 'Is this a value stored on a related object (external key)?',
+			'IsScalar' => 'Is this a value that makes sense in a SQL/OQL expression?',
+			'IsLinkset' => 'Is this a collection (1-N or N-N)?',
+			'IsHierarchicalKey' => 'Is this attribute an external key pointing to the host class?',
+		);
+	}
+}
+
+

+ 1 - 1
core/metamodel.class.php

@@ -4092,7 +4092,7 @@ abstract class MetaModel
 						}
 					}
 				}
-				else if ($oAttDef->IsDirectField())
+				else if ($oAttDef->IsBasedOnDBColumns())
 				{
 					// Check that the values fit the allowed values
 					//