Browse Source

Stop watches - beta (change tracking to be reviewed)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2166 a333f486-631f-4898-b8df-5754b55c2be0
romainq 12 years ago
parent
commit
118b76cf16

+ 347 - 5
core/attributedef.class.inc.php

@@ -132,6 +132,22 @@ abstract class AttributeDefinition
 		return $this->m_sHostClass;
 		return $this->m_sHostClass;
 	}
 	}
 
 
+	public function ListSubItems()
+	{
+		$aSubItems = array();
+		foreach(MetaModel::ListAttributeDefs($this->m_sHostClass) as $sAttCode => $oAttDef)
+		{
+			if ($oAttDef instanceof AttributeSubItem)
+			{
+				if ($oAttDef->Get('target_attcode') == $this->m_sCode)
+				{
+					$aSubItems[$sAttCode] = $oAttDef;
+				}
+			}
+		}
+		return $aSubItems;
+	}
+
 	// Note: I could factorize this code with the parameter management made for the AttributeDef class
 	// Note: I could factorize this code with the parameter management made for the AttributeDef class
 	// to be overloaded
 	// to be overloaded
 	static public function ListExpectedParams()
 	static public function ListExpectedParams()
@@ -174,6 +190,8 @@ abstract class AttributeDefinition
 	public function IsHierarchicalKey() {return false;}
 	public function IsHierarchicalKey() {return false;}
 	public function IsExternalField() {return false;} 
 	public function IsExternalField() {return false;} 
 	public function IsWritable() {return false;} 
 	public function IsWritable() {return false;} 
+	public function LoadInObject() {return true;}
+	public function GetValue($oHostObject){return null;} // must return the value if LoadInObject returns false
 	public function IsNullAllowed() {return true;} 
 	public function IsNullAllowed() {return true;} 
 	public function GetCode() {return $this->m_sCode;} 
 	public function GetCode() {return $this->m_sCode;} 
 
 
@@ -389,16 +407,25 @@ abstract class AttributeDefinition
 		return (string)$sValue;
 		return (string)$sValue;
 	}
 	}
 
 
+	/**
+	 * Override to display the value in the GUI
+	 */	
 	public function GetAsHTML($sValue, $oHostObject = null)
 	public function GetAsHTML($sValue, $oHostObject = null)
 	{
 	{
 		return Str::pure2html((string)$sValue);
 		return Str::pure2html((string)$sValue);
 	}
 	}
 
 
+	/**
+	 * Override to export the value in XML	
+	 */	
 	public function GetAsXML($sValue, $oHostObject = null)
 	public function GetAsXML($sValue, $oHostObject = null)
 	{
 	{
 		return Str::pure2xml((string)$sValue);
 		return Str::pure2xml((string)$sValue);
 	}
 	}
 
 
+	/**
+	 * Override to escape the value when read by DBObject::GetAsCSV()	
+	 */	
 	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
 	{
 	{
 		return (string)$sValue;
 		return (string)$sValue;
@@ -2432,7 +2459,7 @@ class AttributeDuration extends AttributeInteger
 		return Str::pure2html(self::FormatDuration($value));
 		return Str::pure2html(self::FormatDuration($value));
 	}
 	}
 
 
-	static function FormatDuration($duration)
+	public static function FormatDuration($duration)
 	{
 	{
 		$aDuration = self::SplitDuration($duration);
 		$aDuration = self::SplitDuration($duration);
 		$sResult = '';
 		$sResult = '';
@@ -2508,11 +2535,17 @@ class AttributeDeadline extends AttributeDateTime
 {
 {
 	public function GetAsHTML($value, $oHostObject = null)
 	public function GetAsHTML($value, $oHostObject = null)
 	{
 	{
+		$sResult = self::FormatDeadline($value);
+		return $sResult;
+	}
+
+	public static function FormatDeadline($value)
+	{
 		$sResult = '';
 		$sResult = '';
 		if ($value !== null)
 		if ($value !== null)
 		{
 		{
 			$iValue = AttributeDateTime::GetAsUnixSeconds($value);
 			$iValue = AttributeDateTime::GetAsUnixSeconds($value);
-			$sDate = parent::GetAsHTML($value, $oHostObject);
+			$sDate = $value;
 			$difference = $iValue - time();
 			$difference = $iValue - time();
 	
 	
 			if ($difference >= 0)
 			if ($difference >= 0)
@@ -2526,6 +2559,7 @@ class AttributeDeadline extends AttributeDateTime
 			$sFormat = MetaModel::GetConfig()->Get('deadline_format', '$difference$');
 			$sFormat = MetaModel::GetConfig()->Get('deadline_format', '$difference$');
 			$sResult = str_replace(array('$date$', '$difference$'), array($sDate, $sDifference), $sFormat);
 			$sResult = str_replace(array('$date$', '$difference$'), array($sDate, $sDifference), $sFormat);
 		}
 		}
+
 		return $sResult;
 		return $sResult;
 	}
 	}
 
 
@@ -3234,7 +3268,11 @@ class AttributeStopWatch extends AttributeDefinition
 	public function IsScalar() {return true;} 
 	public function IsScalar() {return true;} 
 	public function IsWritable() {return false;} 
 	public function IsWritable() {return false;} 
 	public function GetDefaultValue() {return $this->NewStopWatch();}
 	public function GetDefaultValue() {return $this->NewStopWatch();}
-	//public function IsNullAllowed() {return $this->GetOptional("is_null_allowed", false);}
+
+	public function GetEditValue($value, $oHostObj = null)
+	{
+		return $value->GetTimeSpent();
+	}
 
 
 	public function GetStates()
 	public function GetStates()
 	{
 	{
@@ -3264,6 +3302,24 @@ class AttributeStopWatch extends AttributeDefinition
 		return $proposedValue;
 		return $proposedValue;
 	}
 	}
 
 
+	public function Equals($val1, $val2)
+	{
+		if ($val1 === $val2) return true;
+
+		if (is_object($val1) != is_object($val2))
+		{
+			return false;
+		}
+		if (!is_object($val1))
+		{
+			// string ?
+			return false;
+		}
+
+		// Both values are Object sets
+		return $val1->HasSameContents($val2);
+	}
+
 	public function GetSQLExpressions($sPrefix = '')
 	public function GetSQLExpressions($sPrefix = '')
 	{
 	{
 		if ($sPrefix == '')
 		if ($sPrefix == '')
@@ -3281,6 +3337,7 @@ class AttributeStopWatch extends AttributeDefinition
 			$sThPrefix = '_'.$iThreshold;
 			$sThPrefix = '_'.$iThreshold;
 			$aColumns[$sThPrefix.'_deadline'] = $sPrefix.$sThPrefix.'_deadline';
 			$aColumns[$sThPrefix.'_deadline'] = $sPrefix.$sThPrefix.'_deadline';
 			$aColumns[$sThPrefix.'_passed'] = $sPrefix.$sThPrefix.'_passed';
 			$aColumns[$sThPrefix.'_passed'] = $sPrefix.$sThPrefix.'_passed';
+			$aColumns[$sThPrefix.'_triggered'] = $sPrefix.$sThPrefix.'_triggered';
 			$aColumns[$sThPrefix.'_overrun'] = $sPrefix.$sThPrefix.'_overrun';
 			$aColumns[$sThPrefix.'_overrun'] = $sPrefix.$sThPrefix.'_overrun';
 		}
 		}
 		return $aColumns;
 		return $aColumns;
@@ -3314,6 +3371,7 @@ class AttributeStopWatch extends AttributeDefinition
 			$sThPrefix = '_'.$iThreshold;
 			$sThPrefix = '_'.$iThreshold;
 			$aExpectedCols[] = $sPrefix.$sThPrefix.'_deadline';
 			$aExpectedCols[] = $sPrefix.$sThPrefix.'_deadline';
 			$aExpectedCols[] = $sPrefix.$sThPrefix.'_passed';
 			$aExpectedCols[] = $sPrefix.$sThPrefix.'_passed';
+			$aExpectedCols[] = $sPrefix.$sThPrefix.'_triggered';
 			$aExpectedCols[] = $sPrefix.$sThPrefix.'_overrun';
 			$aExpectedCols[] = $sPrefix.$sThPrefix.'_overrun';
 		}
 		}
 		foreach ($aExpectedCols as $sExpectedCol)
 		foreach ($aExpectedCols as $sExpectedCol)
@@ -3340,6 +3398,7 @@ class AttributeStopWatch extends AttributeDefinition
 				$iThreshold,
 				$iThreshold,
 				self::DateToSeconds($aCols[$sPrefix.$sThPrefix.'_deadline']),
 				self::DateToSeconds($aCols[$sPrefix.$sThPrefix.'_deadline']),
 				(bool)($aCols[$sPrefix.$sThPrefix.'_passed'] == 1),
 				(bool)($aCols[$sPrefix.$sThPrefix.'_passed'] == 1),
+				(bool)($aCols[$sPrefix.$sThPrefix.'_triggered'] == 1),
 				$aCols[$sPrefix.$sThPrefix.'_overrun']
 				$aCols[$sPrefix.$sThPrefix.'_overrun']
 			);
 			);
 		}
 		}
@@ -3362,6 +3421,7 @@ class AttributeStopWatch extends AttributeDefinition
 				$sPrefix = $this->GetCode().'_'.$iThreshold;
 				$sPrefix = $this->GetCode().'_'.$iThreshold;
 				$aValues[$sPrefix.'_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold));
 				$aValues[$sPrefix.'_deadline'] = self::SecondsToDate($value->GetThresholdDate($iThreshold));
 				$aValues[$sPrefix.'_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0';
 				$aValues[$sPrefix.'_passed'] = $value->IsThresholdPassed($iThreshold) ? '1' : '0';
+				$aValues[$sPrefix.'_triggered'] = $value->IsThresholdTriggered($iThreshold) ? '1' : '0';
 				$aValues[$sPrefix.'_overrun'] = $value->GetOverrun($iThreshold);
 				$aValues[$sPrefix.'_overrun'] = $value->GetOverrun($iThreshold);
 			}
 			}
 		}
 		}
@@ -3388,6 +3448,7 @@ class AttributeStopWatch extends AttributeDefinition
 			$sPrefix = $this->GetCode().'_'.$iThreshold;
 			$sPrefix = $this->GetCode().'_'.$iThreshold;
 			$aColumns[$sPrefix.'_deadline'] = 'DATETIME NULL';
 			$aColumns[$sPrefix.'_deadline'] = 'DATETIME NULL';
 			$aColumns[$sPrefix.'_passed'] = 'TINYINT(1) NULL';
 			$aColumns[$sPrefix.'_passed'] = 'TINYINT(1) NULL';
+			$aColumns[$sPrefix.'_triggered'] = 'TINYINT(1) NULL';
 			$aColumns[$sPrefix.'_overrun'] = 'INT(11) UNSIGNED NULL';
 			$aColumns[$sPrefix.'_overrun'] = 'INT(11) UNSIGNED NULL';
 		}
 		}
 		return $aColumns;
 		return $aColumns;
@@ -3395,8 +3456,6 @@ class AttributeStopWatch extends AttributeDefinition
 
 
 	public function GetFilterDefinitions()
 	public function GetFilterDefinitions()
 	{
 	{
-		//return array();
-		// still not working... see later...
 		$aRes = array(
 		$aRes = array(
 			$this->GetCode() => new FilterFromAttribute($this),
 			$this->GetCode() => new FilterFromAttribute($this),
 			$this->GetCode().'_started' => new FilterFromAttribute($this, '_started'),
 			$this->GetCode().'_started' => new FilterFromAttribute($this, '_started'),
@@ -3408,6 +3467,7 @@ class AttributeStopWatch extends AttributeDefinition
 			$sPrefix = $this->GetCode().'_'.$iThreshold;
 			$sPrefix = $this->GetCode().'_'.$iThreshold;
 			$aRes[$sPrefix.'_deadline'] = new FilterFromAttribute($this, '_deadline');
 			$aRes[$sPrefix.'_deadline'] = new FilterFromAttribute($this, '_deadline');
 			$aRes[$sPrefix.'_passed'] = new FilterFromAttribute($this, '_passed');
 			$aRes[$sPrefix.'_passed'] = new FilterFromAttribute($this, '_passed');
+			$aRes[$sPrefix.'_triggered'] = new FilterFromAttribute($this, '_triggered');
 			$aRes[$sPrefix.'_overrun'] = new FilterFromAttribute($this, '_overrun');
 			$aRes[$sPrefix.'_overrun'] = new FilterFromAttribute($this, '_overrun');
 		}
 		}
 		return $aRes;
 		return $aRes;
@@ -3449,6 +3509,288 @@ class AttributeStopWatch extends AttributeDefinition
 	{
 	{
 		return $this->Get('thresholds');
 		return $this->Get('thresholds');
 	}
 	}
+
+	/**
+	 * To expose internal values: Declare an attribute AttributeSubItem
+	 * and implement the GetSubItemXXXX verbs
+	 */	 	
+	public function GetSubItemSQLExpression($sItemCode)
+	{
+		$sPrefix = $this->GetCode();
+		switch($sItemCode)
+		{
+		case 'timespent':
+			return array('' => $sPrefix.'_timespent');
+		case 'started':
+			return array('' => $sPrefix.'_started');
+		case 'laststart':
+			return array('' => $sPrefix.'_laststart');
+		case 'stopped':
+			return array('' => $sPrefix.'_stopped');
+		}
+
+		foreach ($this->ListThresholds() as $iThreshold => $aFoo)
+		{
+			$sThPrefix = $iThreshold.'_';
+			if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
+			{
+				// The current threshold is concerned
+				$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+				switch($sThresholdCode)
+				{
+				case 'deadline':
+					return array('' => $sPrefix.'_'.$iThreshold.'_deadline');
+				case 'passed':
+					return array('' => $sPrefix.'_'.$iThreshold.'_passed');
+				case 'triggered':
+					return array('' => $sPrefix.'_'.$iThreshold.'_triggered');
+				case 'overrun':
+					return array('' => $sPrefix.'_'.$iThreshold.'_overrun');
+				}
+			}
+		}
+		throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode());
+	}
+
+	public function GetSubItemValue($sItemCode, $value, $oHostObject = null)
+	{
+		$oStopWatch = $value;
+		switch($sItemCode)
+		{
+		case 'timespent':
+			return $oStopWatch->GetTimeSpent();
+		case 'started':
+			return $oStopWatch->GetStartDate();
+		case 'laststart':
+			return $oStopWatch->GetLastStartDate();
+		case 'stopped':
+			return $oStopWatch->GetStopDate();
+		}
+
+		foreach ($this->ListThresholds() as $iThreshold => $aFoo)
+		{
+			$sThPrefix = $iThreshold.'_';
+			if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
+			{
+				// The current threshold is concerned
+				$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+				switch($sThresholdCode)
+				{
+				case 'deadline':
+					return $oStopWatch->GetThresholdDate($iThreshold);
+				case 'passed':
+					return $oStopWatch->IsThresholdPassed($iThreshold);
+				case 'triggered':
+					return $oStopWatch->IsThresholdTriggered($iThreshold);
+				case 'overrun':
+					return $oStopWatch->GetOverrun($iThreshold);
+				}
+			}
+		}
+
+		throw new CoreException("Unknown item code '$sItemCode' for attribute ".$this->GetHostClass().'::'.$this->GetCode());
+	}
+
+	static protected function GetDateFormat($bFull = false)
+	{
+		if ($bFull)
+		{
+			return "Y-m-d H:i:s";
+		}
+		else
+		{
+			return "Y-m-d H:i";
+		}
+	}
+
+	public function GetSubItemAsHTML($sItemCode, $value)
+	{
+		$sHtml = $value;
+
+		switch($sItemCode)
+		{
+		case 'timespent':
+			$sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
+			break;
+		case 'started':
+		case 'laststart':
+		case 'stopped':
+			if (is_null($value))
+			{
+				$sHtml = ''; // Undefined
+			}
+			else
+			{
+				$sHtml = date(self::GetDateFormat(), $value);
+			}
+			break;
+
+		default:
+			foreach ($this->ListThresholds() as $iThreshold => $aFoo)
+			{
+				$sThPrefix = $iThreshold.'_';
+				if (substr($sItemCode, 0, strlen($sThPrefix)) == $sThPrefix)
+				{
+					// The current threshold is concerned
+					$sThresholdCode = substr($sItemCode, strlen($sThPrefix));
+					switch($sThresholdCode)
+					{
+					case 'deadline':
+						if ($value)
+						{
+							$sDate = date(self::GetDateFormat(true /*full*/), $value);
+							$sHtml = Str::pure2html(AttributeDeadline::FormatDeadline($sDate));
+						}
+						else
+						{
+							$sHtml = '';
+						}
+						break;
+					case 'passed':
+						$sHtml = $value ? '1' : '0';
+						break;
+					case 'triggered':
+						$sHtml = $value ? '1' : '0';
+						break;
+					case 'overrun':
+						$sHtml = Str::pure2html(AttributeDuration::FormatDuration($value));
+						break;
+					}
+				}
+			}
+		}
+		return $sHtml;
+	}
+
+	public function GetSubItemAsCSV($sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"')
+	{
+		return $value;
+	}
+
+	public function GetSubItemAsXML($sItemCode, $value)
+	{
+		return Str::pure2xml((string)$value);
+	}
+}
+
+/**
+ * View of a subvalue of another attribute
+ * If an attribute implements the verbs GetSubItem.... then it can expose
+ * internal values, each of them being an attribute and therefore they
+ * can be displayed at different times in the object lifecycle, and used for
+ * reporting (as a condition in OQL, or as an additional column in an export)  
+ * Known usages: Stop Watches can expose threshold statuses    
+ */
+class AttributeSubItem extends AttributeDefinition
+{
+	static public function ListExpectedParams()
+	{
+		return array_merge(parent::ListExpectedParams(), array('target_attcode', 'item_code'));
+	}
+
+	public function GetParentAttCode() {return $this->Get("target_attcode");} 
+
+	/**
+	 * Helper : get the attribute definition to which the execution will be forwarded	
+	 */	
+	protected function GetTargetAttDef()
+	{
+		$sClass = $this->GetHostClass();
+		$oParentAttDef = MetaModel::GetAttributeDef($sClass, $this->Get('target_attcode'));
+		return $oParentAttDef;
+	}
+
+	public function GetEditClass() {return "";}
+	
+	public function GetValuesDef() {return null;} 
+	//public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} 
+
+	public function IsDirectField() {return true;} 
+	public function IsScalar() {return true;} 
+	public function IsWritable() {return false;} 
+	public function GetDefaultValue() {return null;}
+//	public function IsNullAllowed() {return false;}
+	public function LoadInObject() {return false;} // if this verb returns true, then GetValue must be implemented
+
+	// 
+//	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
+
+	public function FromSQLToValue($aCols, $sPrefix = '')
+	{
+	}
+
+	public function GetSQLColumns()
+	{
+		return array();
+	}
+
+	public function GetFilterDefinitions()
+	{
+		return array($this->GetCode() => new FilterFromAttribute($this));
+	}
+
+	public function GetBasicFilterOperators()
+	{
+		return array();
+	}
+	public function GetBasicFilterLooseOperator()
+	{
+		return "=";
+	}
+
+	public function GetBasicFilterSQLExpr($sOpCode, $value)
+	{
+		$sQValue = CMDBSource::Quote($value);
+		switch ($sOpCode)
+		{
+		case '!=':
+			return $this->GetSQLExpr()." != $sQValue";
+			break;
+		case '=':
+		default:
+			return $this->GetSQLExpr()." = $sQValue";
+		}
+	} 
+
+	public function GetSQLExpressions($sPrefix = '')
+	{
+		$oParent = $this->GetTargetAttDef();
+		$res = $oParent->GetSubItemSQLExpression($this->Get('item_code'));
+		return $res;
+	}
+
+	/**
+	 * Used by DBOBject::Get()	
+	 */	
+	public function GetValue($parentValue, $oHostObject = null)
+	{
+		$oParent = $this->GetTargetAttDef();
+		$res = $oParent->GetSubItemValue($this->Get('item_code'), $parentValue, $oHostObject);
+		return $res;
+	}
+
+
+	public function GetAsHTML($value, $oHostObject = null)
+	{
+		$oParent = $this->GetTargetAttDef();
+		$res = $oParent->GetSubItemAsHTML($this->Get('item_code'), $value);
+		return $res;
+	}
+
+	public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null)
+	{
+		$oParent = $this->GetTargetAttDef();
+		$res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator = ',', $sTextQualifier = '"');
+		return $res;
+	}
+	
+	public function GetAsXML($value, $oHostObject = null)
+	{
+		$oParent = $this->GetTargetAttDef();
+		$res = $oParent->GetSubItemAsXML($this->Get('item_code'), $value);
+		return $res;
+	}
+
 }
 }
 
 
 /**
 /**

+ 38 - 49
core/cmdbobject.class.inc.php

@@ -133,8 +133,18 @@ abstract class CMDBObject extends DBObject
 		foreach ($aValues as $sAttCode=> $value)
 		foreach ($aValues as $sAttCode=> $value)
 		{
 		{
 			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
 			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
+			if ($oAttDef->IsExternalField()) continue; // #@# temporary
 			if ($oAttDef->IsLinkSet()) continue; // #@# temporary
 			if ($oAttDef->IsLinkSet()) continue; // #@# temporary
 
 
+			if (array_key_exists($sAttCode, $aOrigValues))
+			{
+				$original = $aOrigValues[$sAttCode];
+			}
+			else
+			{
+				$original = null;
+			}
+
 			if ($oAttDef instanceOf AttributeOneWayPassword)
 			if ($oAttDef instanceOf AttributeOneWayPassword)
 			{
 			{
 				// One Way encrypted passwords' history is stored -one way- encrypted
 				// One Way encrypted passwords' history is stored -one way- encrypted
@@ -144,11 +154,7 @@ abstract class CMDBObject extends DBObject
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
 
 
-				if (array_key_exists($sAttCode, $aOrigValues))
-				{
-					$original = $aOrigValues[$sAttCode];
-				}
-				else
+				if (is_null($original))
 				{
 				{
 					$original = '';
 					$original = '';
 				}
 				}
@@ -164,11 +170,7 @@ abstract class CMDBObject extends DBObject
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
 
 
-				if (array_key_exists($sAttCode, $aOrigValues))
-				{
-					$original = $aOrigValues[$sAttCode];
-				}
-				else
+				if (is_null($original))
 				{
 				{
 					$original = '';
 					$original = '';
 				}
 				}
@@ -184,11 +186,7 @@ abstract class CMDBObject extends DBObject
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
 
 
-				if (array_key_exists($sAttCode, $aOrigValues))
-				{
-					$original = $aOrigValues[$sAttCode];
-				}
-				else
+				if (is_null($original))
 				{
 				{
 					$original = new ormDocument();
 					$original = new ormDocument();
 				}
 				}
@@ -197,26 +195,30 @@ abstract class CMDBObject extends DBObject
 			}
 			}
 			elseif ($oAttDef instanceOf AttributeStopWatch)
 			elseif ($oAttDef instanceOf AttributeStopWatch)
 			{
 			{
-				// Stop watches
-				// TEMPORARY IMPLEMENTATION
-				$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
-				$oMyChangeOp->Set("change", $oChange->GetKey());
-				$oMyChangeOp->Set("objclass", get_class($this));
-				$oMyChangeOp->Set("objkey", $this->GetKey());
-				$oMyChangeOp->Set("attcode", $sAttCode);
-
-				// Temporary - working thanks to ormStopWatch::__toString()
-				if (array_key_exists($sAttCode, $aOrigValues))
+				// Stop watches - record changes for sub items only (they are visible, the rest is not visible)
+				//
+				if (is_null($original))
 				{
 				{
-					$sOriginalValue = $aOrigValues[$sAttCode];
+					$original = new OrmStopWatch();
 				}
 				}
-				else
+				foreach ($oAttDef->ListSubItems() as $sSubItemAttCode => $oSubItemAttDef)
 				{
 				{
-					$sOriginalValue = 'undefined';
+					$item_value = $oSubItemAttDef->GetValue($value);
+					$item_original = $oSubItemAttDef->GetValue($original);
+
+					if ($item_value != $item_original)
+					{
+						$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeScalar");
+						$oMyChangeOp->Set("change", $oChange->GetKey());
+						$oMyChangeOp->Set("objclass", get_class($this));
+						$oMyChangeOp->Set("objkey", $this->GetKey());
+						$oMyChangeOp->Set("attcode", $sSubItemAttCode);
+		
+						$oMyChangeOp->Set("oldvalue", $item_original);
+						$oMyChangeOp->Set("newvalue", $item_value);
+						$iId = $oMyChangeOp->DBInsertNoReload();
+					}
 				}
 				}
-				$oMyChangeOp->Set("oldvalue", $sOriginalValue);
-				$oMyChangeOp->Set("newvalue", $value);
-				$iId = $oMyChangeOp->DBInsertNoReload();
 			}
 			}
 			elseif ($oAttDef instanceOf AttributeCaseLog)
 			elseif ($oAttDef instanceOf AttributeCaseLog)
 			{
 			{
@@ -238,17 +240,9 @@ abstract class CMDBObject extends DBObject
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
 
 
-				if (array_key_exists($sAttCode, $aOrigValues))
-				{
-					$original = $aOrigValues[$sAttCode];
-					if ($original instanceof ormCaseLog)
-					{
-						$original = $original->GetText();
-					}
-				}
-				else
+				if (!is_null($original) && ($original instanceof ormCaseLog))
 				{
 				{
-					$original = null;
+					$original = $original->GetText();
 				}
 				}
 				$oMyChangeOp->Set("prevdata", $original);
 				$oMyChangeOp->Set("prevdata", $original);
 				$iId = $oMyChangeOp->DBInsertNoReload();
 				$iId = $oMyChangeOp->DBInsertNoReload();
@@ -262,16 +256,11 @@ abstract class CMDBObject extends DBObject
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objclass", get_class($this));
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("objkey", $this->GetKey());
 				$oMyChangeOp->Set("attcode", $sAttCode);
 				$oMyChangeOp->Set("attcode", $sAttCode);
-
-				if (array_key_exists($sAttCode, $aOrigValues))
-				{
-					$sOriginalValue = $aOrigValues[$sAttCode];
-				}
-				else
+				if (is_null($original))
 				{
 				{
-					$sOriginalValue = 'undefined';
+					$original = 'undefined';
 				}
 				}
-				$oMyChangeOp->Set("oldvalue", $sOriginalValue);
+				$oMyChangeOp->Set("oldvalue", $original);
 				$oMyChangeOp->Set("newvalue", $value);
 				$oMyChangeOp->Set("newvalue", $value);
 				$iId = $oMyChangeOp->DBInsertNoReload();
 				$iId = $oMyChangeOp->DBInsertNoReload();
 			}
 			}

+ 55 - 47
core/dbobject.class.php

@@ -145,6 +145,7 @@ abstract class DBObject
 	{
 	{
 		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
 		foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode=>$oAttDef)
 		{
 		{
+			if (!$oAttDef->LoadInObject()) continue;
 			if (!isset($this->m_aLoadedAtt[$sAttCode]) || !$this->m_aLoadedAtt[$sAttCode])
 			if (!isset($this->m_aLoadedAtt[$sAttCode]) || !$this->m_aLoadedAtt[$sAttCode])
 			{
 			{
 				return false;
 				return false;
@@ -248,6 +249,8 @@ abstract class DBObject
 			// Skip links (could not be loaded by the mean of this query)
 			// Skip links (could not be loaded by the mean of this query)
 			if ($oAttDef->IsLinkSet()) continue;
 			if ($oAttDef->IsLinkSet()) continue;
 
 
+			if (!$oAttDef->LoadInObject()) continue;
+
 			// Note: we assume that, for a given attribute, if it can be loaded,
 			// Note: we assume that, for a given attribute, if it can be loaded,
 			// then one column will be found with an empty suffix, the others have a suffix
 			// then one column will be found with an empty suffix, the others have a suffix
 			// Take care: the function isset will return false in case the value is null,
 			// Take care: the function isset will return false in case the value is null,
@@ -411,68 +414,73 @@ abstract class DBObject
 
 
 	public function GetStrict($sAttCode)
 	public function GetStrict($sAttCode)
 	{
 	{
-		if (!array_key_exists($sAttCode, MetaModel::ListAttributeDefs(get_class($this))))
-		{
-			throw new CoreException("Unknown attribute code '$sAttCode' for the class ".get_class($this));
-		}
+		$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
 
 
-		if (isset($this->m_aLoadedAtt[$sAttCode]))
+		if (!$oAttDef->LoadInObject())
 		{
 		{
-			// Standard case... we have the information directly
-		}
-		elseif ($this->m_bIsInDB && !$this->m_bDirty)
-		{
-			// Lazy load (polymorphism): complete by reloading the entire object
-			// #@# non-scalar attributes.... handle that differently?
-			$this->Reload();
-		}
-		elseif ($sAttCode == 'friendlyname')
-		{
-			// The friendly name is not computed and the object is dirty
-			// Todo: implement the computation of the friendly name based on sprintf()
-			// 
-			$this->m_aCurrValues[$sAttCode] = '';
+			$sParentAttCode = $oAttDef->GetParentAttCode();
+			$parentValue = $this->GetStrict($sParentAttCode);
+			$value = $oAttDef->GetValue($parentValue, $this);
 		}
 		}
 		else
 		else
 		{
 		{
-			// Not loaded... is it related to an external key?
-			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
-			if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+			if (isset($this->m_aLoadedAtt[$sAttCode]))
 			{
 			{
-				// Let's get the object and compute all of the corresponding attributes
-				// (i.e not only the requested attribute)
-				//
-				$sExtKeyAttCode = $oAttDef->GetKeyAttCode();
-
-				if ($iRemote = $this->Get($sExtKeyAttCode))
-				{
-					$oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
-					$oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote);
-				}
-				else
-				{
-					$oRemote = null;
-				}
-
-				foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
+				// Standard case... we have the information directly
+			}
+			elseif ($this->m_bIsInDB && !$this->m_bDirty)
+			{
+				// Lazy load (polymorphism): complete by reloading the entire object
+				// #@# non-scalar attributes.... handle that differently?
+				$this->Reload();
+			}
+			elseif ($sAttCode == 'friendlyname')
+			{
+				// The friendly name is not computed and the object is dirty
+				// Todo: implement the computation of the friendly name based on sprintf()
+				// 
+				$this->m_aCurrValues[$sAttCode] = '';
+			}
+			else
+			{
+				// Not loaded... is it related to an external key?
+				if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
 				{
 				{
-					if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sExtKeyAttCode))
+					// Let's get the object and compute all of the corresponding attributes
+					// (i.e not only the requested attribute)
+					//
+					$sExtKeyAttCode = $oAttDef->GetKeyAttCode();
+	
+					if ($iRemote = $this->Get($sExtKeyAttCode))
 					{
 					{
-						if ($oRemote)
-						{
-							$this->m_aCurrValues[$sCode] = $oRemote->Get($oDef->GetExtAttCode());
-						}
-						else
+						$oExtKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
+						$oRemote = MetaModel::GetObject($oExtKeyAttDef->GetTargetClass(), $iRemote);
+					}
+					else
+					{
+						$oRemote = null;
+					}
+	
+					foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sCode => $oDef)
+					{
+						if (($oDef->IsExternalField() || ($oDef instanceof AttributeFriendlyName)) && ($oDef->GetKeyAttCode() == $sExtKeyAttCode))
 						{
 						{
-							$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue();
+							if ($oRemote)
+							{
+								$this->m_aCurrValues[$sCode] = $oRemote->Get($oDef->GetExtAttCode());
+							}
+							else
+							{
+								$this->m_aCurrValues[$sCode] = $oDef->GetDefaultValue();
+							}
+							$this->m_aLoadedAtt[$sCode] = true;
 						}
 						}
-						$this->m_aLoadedAtt[$sCode] = true;
 					}
 					}
 				}
 				}
 			}
 			}
+			$value = $this->m_aCurrValues[$sAttCode];
 		}
 		}
 
 
-		$value = $this->m_aCurrValues[$sAttCode];
 		if ($value instanceof DBObjectSet)
 		if ($value instanceof DBObjectSet)
 		{
 		{
 			$value->Rewind();
 			$value->Rewind();

+ 1 - 0
core/metamodel.class.php

@@ -2365,6 +2365,7 @@ abstract class MetaModel
 			foreach ($aAttList as $sAttCode => $oAttDef)
 			foreach ($aAttList as $sAttCode => $oAttDef)
 			{
 			{
 				if (!$oAttDef->IsScalar()) continue;
 				if (!$oAttDef->IsScalar()) continue;
+				// keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue;
 				
 				
 				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
 				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
 				{
 				{

+ 87 - 42
core/ormstopwatch.class.inc.php

@@ -54,23 +54,47 @@ class ormStopWatch
 		$this->aThresholds = array();
 		$this->aThresholds = array();
 	}
 	}
 
 
-	public function DefineThreshold($iPercent, $tDeadline = null, $bPassed = false, $iOverrun = 0)
+	// BUGGY - DOES NOT DETECT A CHANGE IN THE DEADLINE
+	//
+	public function HasSameContents($oStopWatch)
+	{
+		if ($oStopWatch->iTimeSpent != $this->iTimeSpent) return false;
+		if ($oStopWatch->iStarted != $this->iStarted) return false;
+		if ($oStopWatch->iLastStart != $this->iLastStart) return false;
+		if ($oStopWatch->iStopped != $this->iStopped) return false;
+		if ($oStopWatch->aThresholds != $this->aThresholds) return false;
+
+		// Array comparison is not recursive... let's do it by myself
+		foreach ($oStopWatch->aThresholds as $iPercent => $aThresholdData)
+		{
+			// Assumption: the thresholds will not change dynamically (defined at application design time)
+			$aThisThresholdData = $this->aThresholds[$iPercent];
+			if ($aThisThresholdData['deadline'] != $aThresholdData['deadline']) return false;
+			if ($aThisThresholdData['passed'] != $aThresholdData['passed']) return false;
+			if ($aThisThresholdData['triggered'] != $aThresholdData['triggered']) return false;
+			if ($aThisThresholdData['overrun'] != $aThresholdData['overrun']) return false;
+		}
+
+return false;
+
+		
+		return true;
+	}
+
+
+	public function DefineThreshold($iPercent, $tDeadline = null, $bPassed = false, $bTriggered = false, $iOverrun = 0)
 	{
 	{
 		$this->aThresholds[$iPercent] = array(
 		$this->aThresholds[$iPercent] = array(
 			'deadline' => $tDeadline, // unix time (seconds)
 			'deadline' => $tDeadline, // unix time (seconds)
 			'passed' => $bPassed,
 			'passed' => $bPassed,
+			'triggered' => $bTriggered,
 			'overrun' => $iOverrun
 			'overrun' => $iOverrun
 		);
 		);
 	}
 	}
 
 
-	public function MarkThresholdAsPassed($iPercent)
-	{
-		$this->aThresholds[$iPercent]['passed'] = true;
-	}
-
-	public function __toString()
+	public function MarkThresholdAsTriggered($iPercent)
 	{
 	{
-		return (string)$this->iTimeSpent;
+		$this->aThresholds[$iPercent]['triggered'] = true;
 	}
 	}
 
 
 	public function GetTimeSpent()
 	public function GetTimeSpent()
@@ -127,6 +151,17 @@ class ormStopWatch
 			return false;
 			return false;
 		}
 		}
 	}
 	}
+	public function IsThresholdTriggered($iPercent)
+	{
+		if (array_key_exists($iPercent, $this->aThresholds))
+		{
+			return $this->aThresholds[$iPercent]['triggered'];
+		}
+		else
+		{
+			return false;
+		}
+	}
 
 
 	public function GetAsHTML($oAttDef, $oHostObject = null)
 	public function GetAsHTML($oAttDef, $oHostObject = null)
 	{
 	{
@@ -157,21 +192,20 @@ class ormStopWatch
 
 
 		foreach ($this->aThresholds as $iPercent => $aThresholdData)
 		foreach ($this->aThresholds as $iPercent => $aThresholdData)
 		{
 		{
-			if ($aThresholdData['passed'])
+			$sThresholdDesc = $oAttDef->SecondsToDate($aThresholdData['deadline']);
+			if ($aThresholdData['triggered'])
 			{
 			{
 				if ($aThresholdData['overrun'])
 				if ($aThresholdData['overrun'])
 				{
 				{
-					$aProperties[$iPercent.'%'] = $oAttDef->SecondsToDate($aThresholdData['deadline'])." <b>PASSED</b> by ".$aThresholdData['overrun']." seconds";
+					$sThresholdDesc .= " <b>TRIGGERED</b>, Overrun:".(int) $aThresholdData['overrun']." seconds";
 				}
 				}
 				else
 				else
 				{
 				{
-					$aProperties[$iPercent.'%'] = $oAttDef->SecondsToDate($aThresholdData['deadline'])." <b>PASSED</b>";
+					// Still active, overrun unknown
+					$sThresholdDesc .= " <b>TRIGGERED</b>";
 				}
 				}
 			}
 			}
-			else
-			{
-				$aProperties[$iPercent.'%'] = $oAttDef->SecondsToDate($aThresholdData['deadline']);
-			}
+			$aProperties[$iPercent.'%'] = $sThresholdDesc;
 		}
 		}
 		$sRes = "<TABLE class=\"listResults\">";
 		$sRes = "<TABLE class=\"listResults\">";
 		$sRes .= "<TBODY>";
 		$sRes .= "<TBODY>";
@@ -247,6 +281,7 @@ class ormStopWatch
 		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
 		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
 		{
 		{
 			$aThresholdData['passed'] = false;
 			$aThresholdData['passed'] = false;
+			$aThresholdData['triggered'] = false;
 			$aThresholdData['deadline'] = null;
 			$aThresholdData['deadline'] = null;
 			$aThresholdData['overrun'] = null;
 			$aThresholdData['overrun'] = null;
 		}
 		}
@@ -289,21 +324,31 @@ class ormStopWatch
 		$iDurationGoal = $this->ComputeGoal($oObject, $oAttDef);
 		$iDurationGoal = $this->ComputeGoal($oObject, $oAttDef);
 		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
 		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
 		{
 		{
-			if (!$aThresholdData['passed'])
+			if (is_null($iDurationGoal))
 			{
 			{
-				if (is_null($iDurationGoal))
-				{
-					// No limit: leave null thresholds
-					$aThresholdData['deadline'] = null;
-				}
-				else
-				{
-					$iThresholdDuration = round($iPercent * $iDurationGoal / 100);
-					$aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $this->iLastStart, $iThresholdDuration - $this->iTimeSpent);
-					// OR $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $this->iStarted, $iThresholdDuration);
-				}
+				// No limit: leave null thresholds
+				$aThresholdData['deadline'] = null;
+			}
+			else
+			{
+				$iThresholdDuration = round($iPercent * $iDurationGoal / 100);
+				$aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $this->iLastStart, $iThresholdDuration - $this->iTimeSpent);
+				// OR $aThresholdData['deadline'] = $this->ComputeDeadline($oObject, $oAttDef, $this->iStarted, $iThresholdDuration);
+
+			}
+			if (is_null($aThresholdData['deadline']) || ($aThresholdData['deadline'] > time()))
+			{
+				// The threshold is in the future, reset
+				$aThresholdData['passed'] = false;
+				$aThresholdData['triggered'] = false;
+			}
+			else
+			{
+				// The new threshold is in the past
+				$aThresholdData['passed'] = true;
 			}
 			}
 		}
 		}
+
 		return true;
 		return true;
 	}
 	}
 
 
@@ -323,21 +368,21 @@ class ormStopWatch
 
 
 		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
 		foreach ($this->aThresholds as $iPercent => &$aThresholdData)
 		{
 		{
-			if ($aThresholdData['passed'])
+			if (!is_null($aThresholdData['deadline']) && (time() > $aThresholdData['deadline']))
 			{
 			{
-				if (is_null($aThresholdData['overrun']))
+				$aThresholdData['passed'] = true;
+				if ($aThresholdData['overrun'] > 0)
 				{
 				{
-					// First stop after the deadline is passed
-					$iOverrun = $this->ComputeDuration($oObject, $oAttDef, $aThresholdData['deadline'], time());
-					$aThresholdData['overrun'] = $iOverrun;
+					// Accumulate from last start
+					$aThresholdData['overrun'] += $iElapsed;
 				}
 				}
 				else
 				else
 				{
 				{
-					// Accumulate from last start
-					$aThresholdData['overrun'] += $iElapsed;
+					// First stop after the deadline has been passed
+					$iOverrun = $this->ComputeDuration($oObject, $oAttDef, $aThresholdData['deadline'], time());
+					$aThresholdData['overrun'] = $iOverrun;
 				}
 				}
 			}
 			}
-			$aThresholdData['deadline'] = null;
 		}
 		}
 
 
 		$this->iLastStart = null;
 		$this->iLastStart = null;
@@ -372,8 +417,8 @@ class CheckStopWatchThresholds implements iBackgroundProcess
 					{
 					{
 						$iPercent = $aThresholdData['percent']; // could be different than the index !
 						$iPercent = $aThresholdData['percent']; // could be different than the index !
 		
 		
-						$sExpression = "SELECT $sClass WHERE {$sAttCode}_{$iThreshold}_passed = 0 AND {$sAttCode}_{$iThreshold}_deadline < NOW()";
-						echo $sExpression."<br/>\n";
+						$sExpression = "SELECT $sClass WHERE {$sAttCode}_laststart AND {$sAttCode}_{$iThreshold}_triggered = 0 AND {$sAttCode}_{$iThreshold}_deadline < NOW()";
+						//echo $sExpression."<br/>\n";
 						$oFilter = DBObjectSearch::FromOQL($sExpression);
 						$oFilter = DBObjectSearch::FromOQL($sExpression);
 						$aList = array();
 						$aList = array();
 						$oSet = new DBObjectSet($oFilter);
 						$oSet = new DBObjectSet($oFilter);
@@ -382,7 +427,7 @@ class CheckStopWatchThresholds implements iBackgroundProcess
 							$sClass = get_class($oObj);
 							$sClass = get_class($oObj);
 
 
 							$aList[] = $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold;
 							$aList[] = $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold;
-							echo $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold."<br/>\n";
+							//echo $sClass.'::'.$oObj->GetKey().' '.$sAttCode.' '.$iThreshold."\n";
 
 
 							// Execute planned actions
 							// Execute planned actions
 							//
 							//
@@ -396,10 +441,10 @@ class CheckStopWatchThresholds implements iBackgroundProcess
 								call_user_func_array($aCallSpec, $aParams);
 								call_user_func_array($aCallSpec, $aParams);
 							}
 							}
 
 
-							// Mark the threshold as "passed"
+							// Mark the threshold as "triggered"
 							//
 							//
 							$oSW = $oObj->Get($sAttCode);
 							$oSW = $oObj->Get($sAttCode);
-							$oSW->MarkThresholdAsPassed($iThreshold);
+							$oSW->MarkThresholdAsTriggered($iThreshold);
 							$oObj->Set($sAttCode, $oSW);
 							$oObj->Set($sAttCode, $oSW);
 		
 		
 							if($oObj->IsModified())
 							if($oObj->IsModified())
@@ -407,7 +452,7 @@ class CheckStopWatchThresholds implements iBackgroundProcess
 								// Todo - factorize so that only one single change will be instantiated
 								// Todo - factorize so that only one single change will be instantiated
 								$oMyChange = new CMDBChange();
 								$oMyChange = new CMDBChange();
 								$oMyChange->Set("date", time());
 								$oMyChange->Set("date", time());
-								$oMyChange->Set("userinfo", "Automatic - threshold passed");
+								$oMyChange->Set("userinfo", "Automatic - threshold triggered");
 								$iChangeId = $oMyChange->DBInsertNoReload();
 								$iChangeId = $oMyChange->DBInsertNoReload();
 		
 		
 								$oObj->DBUpdateTracked($oMyChange, true /*skip security*/);
 								$oObj->DBUpdateTracked($oMyChange, true /*skip security*/);
@@ -432,7 +477,7 @@ class CheckStopWatchThresholds implements iBackgroundProcess
 		}
 		}
 
 
 		$iProcessed = count($aList);
 		$iProcessed = count($aList);
-		return "Encountered $iProcessed passed threshold(s)";
+		return "Triggered $iProcessed threshold(s)";
 	}
 	}
 }
 }
 
 

+ 5 - 0
setup/compiler.class.inc.php

@@ -724,6 +724,11 @@ EOF;
 				}
 				}
 				$aParameters['thresholds'] = 'array('.implode(', ', $aThresholds).')';
 				$aParameters['thresholds'] = 'array('.implode(', ', $aThresholds).')';
 			}
 			}
+			elseif ($sAttType == 'AttributeSubItem')
+			{
+				$aParameters['target_attcode'] = $this->GetPropString($oField, 'target_attcode');
+				$aParameters['item_code'] = $this->GetPropString($oField, 'item_code');
+			}
 			else
 			else
 			{
 			{
 				$aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')"
 				$aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')"