Bläddra i källkod

Trac #86 - CSV format aligned with standard specifications
Trac #93 - Fixed issue within the setup data load (related to memory_limit)
Fixed issues with the consultant toolkit: upgrade an existing DB (add new class/attribute)
Developed core services to allow for demonstrating impact computation capability
Deprecated option operation=direct on page UI.php

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

romainq 15 år sedan
förälder
incheckning
0dd4dbc638

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

@@ -455,8 +455,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 		{
 			$aHeader[] = MetaModel::GetLabel($sClassName, $sAttCode);
 		}
-		$sHtml = '#'.$oSet->GetFilter()->ToOQL()."\n";
-		$sHtml .= implode($sSeparator, $aHeader)."\n";
+		$sHtml = implode($sSeparator, $aHeader)."\n";
 		$oSet->Seek(0);
 		while ($oObj = $oSet->Fetch())
 		{
@@ -464,7 +463,7 @@ abstract class cmdbAbstractObject extends CMDBObject
 			$aRow[] = $oObj->GetKey();
 			foreach($aList as $sAttCode)
 			{
-				$aRow[] = $oObj->GetAsCSV($sAttCode, $sSeparator, '\\');
+				$aRow[] = $oObj->GetAsCSV($sAttCode, $sSeparator, $sTextQualifier);
 			}
 			$sHtml .= implode($sSeparator, $aRow)."\n";
 		}

+ 1 - 1
application/nicewebpage.class.inc.php

@@ -53,7 +53,7 @@ class NiceWebPage extends WebPage
 		foreach($aChoices as $sKey => $sValue)
 		{
 			$sSelected = ($sKey == $sDefaultValue) ? " SELECTED" : "";
-			$this->add("<option style=\"width: ".$iWidthPx." px;\" value=\"$sKey\"$sSelected>$sValue</option>");
+			$this->add("<option style=\"width: ".$iWidthPx." px;\" value=\"".htmlspecialchars($sKey)."\"$sSelected>".htmlentities($sValue)."</option>");
 		}
 		$this->add("</select>");
 	}

+ 48 - 14
core/attributedef.class.inc.php

@@ -61,6 +61,7 @@ abstract class AttributeDefinition
 	private $m_aParams = array();
 	private $m_sHostClass = array();
 	protected function Get($sParamName) {return $this->m_aParams[$sParamName];}
+	protected function IsParam($sParamName) {return (array_key_exists($sParamName, $this->m_aParams));}
 	
 	public function __construct($sCode, $aParams)
 	{
@@ -194,9 +195,9 @@ abstract class AttributeDefinition
 		return Str::pure2xml((string)$sValue);
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ';', $sSepEscape = ',')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
 	{
-		return str_replace($sSeparator, $sSepEscape, (string)$sValue);
+		return (string)$sValue;
 	}
 
 	public function GetAllowedValues($aArgs = array(), $sBeginsWith = '')
@@ -233,7 +234,34 @@ class AttributeLinkedSet extends AttributeDefinition
 
 	public function GetValuesDef() {return $this->Get("allowed_values");} 
 	public function GetPrerequisiteAttributes() {return $this->Get("depends_on");} 
-	public function GetDefaultValue() {return DBObjectSet::FromScratch($this->Get('linked_class'));}
+	public function GetDefaultValue($aArgs = array())
+	{
+		// Note: so far, this feature is a prototype,
+		//       later, the argument 'this' should always be present in the arguments
+		//       
+		if (($this->IsParam('default_value')) && array_key_exists('this', $aArgs))
+		{
+			$oSet = $this->Get('default_value');
+			return $oSet->GetValues($aArgs);
+		}
+		else
+		{
+			return DBObjectSet::FromScratch($this->Get('linked_class'));
+		}
+	}
+
+	public function GetSupportedRelations()
+	{
+		if (array_key_exists('supported_relations', $this->m_aParams))
+		{
+			$aSupportedRelations = $this->Get('supported_relations');
+			return $aSupportedRelations;
+		}
+		else
+		{
+			return array();
+		}
+	}
 
 	public function GetLinkedClass() {return $this->Get('linked_class');}
 	public function GetExtKeyToMe() {return $this->Get('ext_key_to_me');}
@@ -252,7 +280,7 @@ class AttributeLinkedSet extends AttributeDefinition
 		return "ERROR: LIST OF OBJECTS";
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ';', $sSepEscape = ',')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
 	{
 		return "ERROR: LIST OF OBJECTS";
 	}
@@ -598,6 +626,14 @@ class AttributeString extends AttributeDBField
 	{
 		return $value;
 	}
+
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
+	{
+		$sFrom = array("\r\n", $sTextQualifier);
+		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
+		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+		return '"'.$sEscaped.'"';
+	}
 }
 
 /**
@@ -678,11 +714,6 @@ class AttributeText extends AttributeString
 	{
 		return Str::pure2xml($value);
 	}
-
-	public function GetAsCSV($value, $sSeparator = ';', $sSepEscape = ',')
-	{
-		return str_replace("\n", "[newline]", parent::GetAsCSV($sValue, $sSeparator, $sSepEscape));
-	}
 }
 
 /**
@@ -982,9 +1013,12 @@ class AttributeDate extends AttributeDBField
 		return Str::pure2xml($value);
 	}
 
-	public function GetAsCSV($value, $sSeparator = ';', $sSepEscape = ',')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
 	{
-		return str_replace($sSeparator, $sSepEscape, $value);
+		$sFrom = array("\r\n", $sTextQualifier);
+		$sTo = array("\n", $sTextQualifier.$sTextQualifier);
+		$sEscaped = str_replace($sFrom, $sTo, (string)$sValue);
+		return '"'.$sEscaped.'"';
 	}
 }
 
@@ -1231,10 +1265,10 @@ class AttributeExternalField extends AttributeDefinition
 		$oExtAttDef = $this->GetExtAttDef();
 		return $oExtAttDef->GetAsXML($value);
 	}
-	public function GetAsCSV($value, $sSeparator = ';', $sSepEscape = ',')
+	public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"')
 	{
 		$oExtAttDef = $this->GetExtAttDef();
-		return $oExtAttDef->GetAsCSV($value);
+		return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier);
 	}
 }
 
@@ -1394,7 +1428,7 @@ class AttributeBlob extends AttributeDefinition
 		}
 	}
 
-	public function GetAsCSV($sValue, $sSeparator = ';', $sSepEscape = ',')
+	public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"')
 	{
 		return ''; // Not exportable in CSV !
 	}

+ 119 - 119
core/csvparser.class.inc.php

@@ -1,8 +1,7 @@
 <?php
-
 /**
  * CSVParser
- * CSV interpreter helper, optionaly tries to guess column mapping and the separator, check the consistency 
+ * CSV interpreter helper 
  *
  * @package     iTopORM
  * @author      Romain Quetiez <romainquetiez@yahoo.fr>
@@ -19,6 +18,16 @@ class CSVParserException extends CoreException
 
 
 
+define('stSTARTING', 1); //grey zone: the type is undetermined
+define('stRAW', 2); //building a non-qualified string
+define('stQUALIFIED', 3); //building qualified string
+define('stESCAPED', 4); //just encountered an escape char
+
+define('evSEPARATOR', 1);
+define('evNEWLINE', 2);
+define('evTEXTQUAL', 3); // used for escaping as well
+define('evOTHERCHAR', 4);
+
 
 /**
  * CSVParser
@@ -34,157 +43,148 @@ class CSVParser
 {
 	private $m_sCSVData;
 	private $m_sSep;
-	private $m_iSkip;
+	private $m_sTextQualifier;
 
-	public function __construct($sTxt)
+	public function __construct($sTxt, $sSep = ',', $sTextQualifier = '"')
 	{
-		$this->m_sCSVData = $sTxt;
+		$this->m_sCSVData = str_replace("\r\n", "\n", $sTxt);
+		$this->m_sSep = $sSep;
+		$this->m_sTextQualifier = $sTextQualifier;
 	}
 
-	public function SetSeparator($sSep)
+	protected $m_sCurrCell = '';
+	protected $m_aCurrRow = array();
+	protected $m_iToSkip = 0;
+	protected $m_aDataSet = array();
+
+	protected function __AddChar($c)
 	{
-		$this->m_sSep = $sSep;
+		$this->m_sCurrCell .= $c;
 	}
-	public function GetSeparator()
+	protected function __ClearCell()
 	{
-		return $this->m_sSep;
+		$this->m_sCurrCell = '';
 	}
-
-	public function SetSkipLines($iSkip)
+	protected function __AddCell($c = null, $aFieldMap = null)
 	{
-		$this->m_iSkip = $iSkip;
+		if (!is_null($aFieldMap))
+		{
+			$iNextCol = count($this->m_aCurrRow);
+			$iNextName = $aFieldMap[$iNextCol];
+			$this->m_aCurrRow[$iNextName] = $this->m_sCurrCell;
+		}
+		else
+		{
+			$this->m_aCurrRow[] = $this->m_sCurrCell;
+		}
+		$this->m_sCurrCell = '';
 	}
-	public function GetSkipLines()
+	protected function __AddRow($c = null, $aFieldMap = null)
 	{
-		return $this->m_iSkip;
-	}
+		$this->__AddCell($c, $aFieldMap);
 
-	public function GuessSeparator()
-	{
-		// Note: skip the first line anyway
-	
-		$aKnownSeps = array(';', ',', "\t"); // Use double quote for special chars!!!
-		$aStatsBySeparator = array();
-		foreach ($aKnownSeps as $sSep)
+		if ($this->m_iToSkip > 0)
 		{
-			$aStatsBySeparator[$sSep] = array();
+			$this->m_iToSkip--;
 		}
-	
-		foreach(explode("\n", $this->m_sCSVData) as $sLine)
+		elseif (count($this->m_aCurrRow) > 1)
 		{
-			$sLine = trim($sLine);
-			if (substr($sLine, 0, 1) == '#') continue;
-			if (empty($sLine)) continue;
-	
-			$aLineCharsCount = count_chars($sLine, 0);
-			foreach ($aKnownSeps as $sSep)
-			{
-				$aStatsBySeparator[$sSep][] = $aLineCharsCount[ord($sSep)];
-			}
+			$this->m_aDataSet[] = $this->m_aCurrRow;
 		}
-	
-		// Default to ','
-		$this->SetSeparator(",");
-
-		foreach ($aKnownSeps as $sSep)
+		elseif ((count($this->m_aCurrRow) == 1) && (strlen($this->m_aCurrRow[0]) > 0))
 		{
-			// Note: this function is NOT available :-( 
-			// stats_variance($aStatsBySeparator[$sSep]);
-			$iMin = min($aStatsBySeparator[$sSep]);
-			$iMax = max($aStatsBySeparator[$sSep]);
-			if (($iMin == $iMax) && ($iMax > 0))
-			{
-				$this->SetSeparator($sSep);
-				break;
-			}
+			$this->m_aDataSet[] = $this->m_aCurrRow;
 		}
-		return $this->GetSeparator();
-	}
-
-	public function GuessSkipLines()
-	{
-		// Take the FIRST -valuable- LINE ONLY
-		// If there is a number, then for sure this is not a header line
-		// Otherwise, we may consider that there is one line to skip
-		foreach(explode("\n", $this->m_sCSVData) as $sLine)
+		else
 		{
-			$sLine = trim($sLine);
-			if (substr($sLine, 0, 1) == '#') continue;
-			if (empty($sLine)) continue;
-	
-			foreach (explode($this->m_sSep, $sLine) as $value)
-			{
-				if (is_numeric($value))
-				{
-					$this->SetSkipLines(0);
-					return 0;
-				}
-			}
-			$this->SetSkipLines(1);
-			return 1;
+			// blank line, skip silently
 		}
+		$this->m_aCurrRow = array();
 	}
 
-	function ToArray($aFieldMap = null, $iMax = 0)
+	function ToArray($iToSkip = 1, $aFieldMap = null, $iMax = 0)
 	{
-		// $aFieldMap is an array of col_index=>col_name
-		// $iMax is to limit the count of rows computed
-		$aRes = array();
-	
-		$iCount = 0;
-		$iSkipped = 0;
-		foreach(explode("\n", $this->m_sCSVData) as $sLine)
+		$aTransitions = array();
+
+		$aTransitions[stSTARTING][evSEPARATOR] = array('__AddCell', stSTARTING);
+		$aTransitions[stSTARTING][evNEWLINE] = array('__AddRow', stSTARTING);
+		$aTransitions[stSTARTING][evTEXTQUAL] = array('', stQUALIFIED);
+		$aTransitions[stSTARTING][evOTHERCHAR] = array('__AddChar', stRAW);
+
+		$aTransitions[stRAW][evSEPARATOR] = array('__AddCell', stSTARTING);
+		$aTransitions[stRAW][evNEWLINE] = array('__AddRow', stSTARTING);
+		$aTransitions[stRAW][evTEXTQUAL] = array('__AddChar', stRAW);
+		$aTransitions[stRAW][evOTHERCHAR] = array('__AddChar', stRAW);
+
+		$aTransitions[stQUALIFIED][evSEPARATOR] = array('__AddChar', stQUALIFIED);
+		$aTransitions[stQUALIFIED][evNEWLINE] = array('__AddChar', stQUALIFIED);
+		$aTransitions[stQUALIFIED][evTEXTQUAL] = array('', stESCAPED);
+		$aTransitions[stQUALIFIED][evOTHERCHAR] = array('__AddChar', stQUALIFIED);
+
+		$aTransitions[stESCAPED][evSEPARATOR] = array('__AddCell', stSTARTING);
+		$aTransitions[stESCAPED][evNEWLINE] = array('__AddRow', stSTARTING);
+		$aTransitions[stESCAPED][evTEXTQUAL] = array('__AddChar', stQUALIFIED);
+		$aTransitions[stESCAPED][evOTHERCHAR] = array('__AddChar', stSTARTING);
+
+		// Reset parser variables
+		$this->m_sCurrCell = '';
+		$this->m_aCurrRow = array();
+		$this->m_iToSkip = $iToSkip;
+		$this->m_aDataSet = array();
+
+		$iState = stSTARTING;
+		for($i = 0; $i < strlen($this->m_sCSVData) ; $i++)
 		{
-			$sLine = trim($sLine);
-			if (substr($sLine, 0, 1) == '#') continue;
-			if (empty($sLine)) continue;
-	
-			if ($iSkipped < $this->m_iSkip)
+			$c = $this->m_sCSVData[$i];
+
+//			// Note: I did that because the unit test was not working fine (file edited with notepad: \n chars padded :-(
+//			if (ord($c) == 0) continue;
+
+			if ($c == $this->m_sSep)
 			{
-				$iSkipped++;
-				continue;
+				$iEvent = evSEPARATOR;
 			}
-	
-			foreach (explode($this->m_sSep, $sLine) as $iCol=>$sValue)
+			elseif ($c == "\n")
 			{
-				if (is_array($aFieldMap)) $sColRef = $aFieldMap[$iCol];
-				else                      $sColRef = $iCol;
-				$aRes[$iCount][$sColRef] = $sValue;
+				$iEvent = evNEWLINE;
 			}
-	
-			$iCount++;
-			if (($iMax > 0) && ($iCount >= $iMax)) break;
-		}
-		return $aRes;
-	}
-
-	public function ListFields()
-	{
-		// Take the first valuable line
-		foreach(explode("\n", $this->m_sCSVData) as $sLine)
-		{
-			$sLine = trim($sLine);
-			if (substr($sLine, 0, 1) == '#') continue;
-			if (empty($sLine)) continue;
-			// We've got the first valuable line, that's it!
-			break;
-		}
-
-		$aRet = array();
-		foreach (explode($this->m_sSep, $sLine) as $iCol=>$value)
-		{
-			if ($this->m_iSkip == 0)
+			elseif ($c == $this->m_sTextQualifier)
 			{
-				// No header to help us
-				$sLabel = "field $iCol";
+				$iEvent = evTEXTQUAL;
 			}
 			else
 			{
-				$sLabel = "$value";
+				$iEvent = evOTHERCHAR;
+			}
+
+			$sAction = $aTransitions[$iState][$iEvent][0];
+			$iState = $aTransitions[$iState][$iEvent][1];
+
+			if (!empty($sAction))
+			{
+				$aCallSpec = array($this, $sAction);
+				if (is_callable($aCallSpec))
+				{
+					call_user_func($aCallSpec, $c, $aFieldMap);
+				}
+				else
+				{
+					throw new CSVParserException("CSVParser: unknown verb '$sAction'");
+				}
 			}
-			$aRet[] = $sLabel;
+
+			$iLineCount = count($this->m_aDataSet);
+			if (($iMax > 0) && ($iLineCount >= $iMax)) break;
 		}
-		return $aRet;
+		// Close the final line
+		$this->__AddRow(null, $aFieldMap);
+		return $this->m_aDataSet;
+	}
+
+	public function ListFields()
+	{
+		$aHeader = $this->ToArray(0, null, 1);
+		return $aHeader[0];
 	}
 }
 

+ 35 - 2
core/dbobject.class.php

@@ -381,10 +381,10 @@ abstract class DBObject
 		return $oAtt->GetAsXML($this->Get($sAttCode));
 	}
 
-	public function GetAsCSV($sAttCode, $sSeparator = ';', $sSepEscape = ',')
+	public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"')
 	{
 		$oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
-		return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sSepEscape);
+		return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier);
 	}
 
 	protected static function MakeHyperLink($sObjClass, $sObjKey, $aAvailableFields)
@@ -883,6 +883,7 @@ abstract class DBObject
 		$aScalarArgs = array();
 		$aScalarArgs[$sArgName] = $this->GetKey();
 		$aScalarArgs[$sArgName.'->id'] = $this->GetKey();
+		$aScalarArgs[$sArgName.'->object()'] = $this;
 		$aScalarArgs[$sArgName.'->hyperlink()'] = $this->GetHyperlink();
 		$aScalarArgs[$sArgName.'->name()'] = $this->GetName();
 	
@@ -903,6 +904,38 @@ abstract class DBObject
 	
 	public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array())
 	{
+		foreach (MetaModel::GetLinkedSets($sClass) as $sAttCode => $oAttDef)
+		{
+			$aSupportedRelations = $oAttDef->GetSupportedRelations();
+			if (!array_key_exists($sRelCode, $aSupportedRelations)) continue; //skip
+
+			$bPropagate = true; // #@# Todo: discuss that setting
+			$iDepth = $bPropagate ? $iMaxDepth - 1 : 0;
+
+			$oNeighbors = $this->Get($sAttCode);
+			while ($oObj = $oObjSet->Fetch())
+			{
+				$sRootClass = MetaModel::GetRootClass(get_class($oObj));
+				$sObjKey = $oObj->GetKey();
+				if (array_key_exists($sRootClass, $aResults))
+				{
+					if (array_key_exists($sObjKey, $aResults[$sRootClass]))
+					{
+						continue; // already visited, skip
+					}
+				}
+
+				$aResults[$sRootClass][$sObjKey] = $oObj;
+				if ($iDepth > 0)
+				{
+					$oObj->GetRelatedObjects($sRelCode, $iDepth, $aResults);
+				}
+			}
+		}
+		
+		return;
+		
+		// #@# todo : Discuss the Relations and the way they are defined (do we deprecate the queries ? what are the properties -e.g. depth- and where do we set them ?)
 		foreach (MetaModel::EnumRelationQueries(get_class($this), $sRelCode) as $sDummy => $aQueryInfo)
 		{
 			MetaModel::DbgTrace("object=".$this->GetKey().", depth=$iMaxDepth, rel=".$aQueryInfo["sQuery"]);

+ 38 - 22
core/metamodel.class.php

@@ -458,6 +458,19 @@ abstract class MetaModel
 		return $aExtKeys;
 	}
 
+	final static public function GetLinkedSets($sClass)
+	{
+		$aLinkedSets = array();
+		foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
+		{
+			if (is_subclass_of($oAtt, 'AttributeLinkedSet'))
+			{
+				$aLinkedSets[$sAttCode] = $oAtt;
+			}
+		}
+		return $aLinkedSets;
+	}
+
 	final static public function GetExternalFields($sClass, $sKeyAttCode)
 	{
 		$aExtFields = array();
@@ -2039,13 +2052,16 @@ abstract class MetaModel
 		list($aErrors, $aSugFix) = self::DBCheckFormat();
 
 		$aSQL = array();
-		foreach ($aSugFix as $sClass => $aQueries)
+		foreach ($aSugFix as $sClass => $aTarget)
 		{
-			foreach ($aQueries as $sQuery)
+			foreach ($aTarget as $aQueries)
 			{
-				//$aSQL[] = $sQuery;
-				// forces a refresh of cached information
-				CMDBSource::CreateTable($sQuery);
+				foreach ($aQueries as $sQuery)
+				{
+					//$aSQL[] = $sQuery;
+					// forces a refresh of cached information
+					CMDBSource::CreateTable($sQuery);
+				}
 			}
 		}
 		// does not work -how to have multiple statements in a single query?
@@ -2078,15 +2094,15 @@ abstract class MetaModel
 			$sAutoIncrement = (self::IsAutoIncrementKey($sClass) ? "AUTO_INCREMENT" : "");
 			if (!CMDBSource::IsTable($sTable))
 			{
-				$aErrors[$sClass][] = "table '$sTable' could not be found into the DB";
-				$aSugFix[$sClass][] = "CREATE TABLE `$sTable` (`$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY) ENGINE = innodb CHARACTER SET utf8 COLLATE utf8_unicode_ci";
+				$aErrors[$sClass]['*'][] = "table '$sTable' could not be found into the DB";
+				$aSugFix[$sClass]['*'][] = "CREATE TABLE `$sTable` (`$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY) ENGINE = innodb CHARACTER SET utf8 COLLATE utf8_unicode_ci";
 			}
 			// Check that the key field exists
 			//
 			elseif (!CMDBSource::IsField($sTable, $sKeyField))
 			{
-				$aErrors[$sClass][] = "key '$sKeyField' (table $sTable) could not be found";
-				$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD `$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY";
+				$aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) could not be found";
+				$aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` ADD `$sKeyField` INT(11) NOT NULL $sAutoIncrement PRIMARY KEY";
 			}
 			else
 			{
@@ -2094,13 +2110,13 @@ abstract class MetaModel
 				//
 				if (!CMDBSource::IsKey($sTable, $sKeyField))
 				{
-					$aErrors[$sClass][] = "key '$sKeyField' is not a key for table '$sTable'";
-					$aSugFix[$sClass][] = "ALTER TABLE `$sTable`, DROP PRIMARY KEY, ADD PRIMARY key(`$sKeyField`)";
+					$aErrors[$sClass]['id'][] = "key '$sKeyField' is not a key for table '$sTable'";
+					$aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable`, DROP PRIMARY KEY, ADD PRIMARY key(`$sKeyField`)";
 				}
 				if (self::IsAutoIncrementKey($sClass) && !CMDBSource::IsAutoIncrement($sTable, $sKeyField))
 				{
-					$aErrors[$sClass][] = "key '$sKeyField' (table $sTable) is not automatically incremented";
-					$aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sKeyField` `$sKeyField` INT(11) NOT NULL AUTO_INCREMENT";
+					$aErrors[$sClass]['id'][] = "key '$sKeyField' (table $sTable) is not automatically incremented";
+					$aSugFix[$sClass]['id'][] = "ALTER TABLE `$sTable` CHANGE `$sKeyField` `$sKeyField` INT(11) NOT NULL AUTO_INCREMENT";
 				}
 			}
 			
@@ -2118,11 +2134,11 @@ abstract class MetaModel
 					$sFieldSpecs = $oAttDef->IsNullAllowed() ? "$sDBFieldType NULL" : "$sDBFieldType NOT NULL";
 					if (!CMDBSource::IsField($sTable, $sField))
 					{
-						$aErrors[$sClass][] = "field '$sField' could not be found in table '$sTable'";
-						$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD `$sField` $sFieldSpecs";
+						$aErrors[$sClass][$sAttCode][] = "field '$sField' could not be found in table '$sTable'";
+						$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD `$sField` $sFieldSpecs";
 						if ($oAttDef->IsExternalKey())
 						{
-							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
+							$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
 						}
 					}
 					else
@@ -2135,30 +2151,30 @@ abstract class MetaModel
 							$bToBeChanged  = true;
 							if ($oAttDef->IsNullAllowed())
 							{
-								$aErrors[$sClass][] = "field '$sField' in table '$sTable' could be NULL";
+								$aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' could be NULL";
 							}
 							else
 							{
-								$aErrors[$sClass][] = "field '$sField' in table '$sTable' could NOT be NULL";
+								$aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' could NOT be NULL";
 							}
 						}
 						$sActualFieldType = CMDBSource::GetFieldType($sTable, $sField);
 						if (strcasecmp($sDBFieldType, $sActualFieldType) != 0)
 						{
 							$bToBeChanged  = true;
-							$aErrors[$sClass][] = "field '$sField' in table '$sTable' has a wrong type: found '$sActualFieldType' while expecting '$sDBFieldType'";
+							$aErrors[$sClass][$sAttCode][] = "field '$sField' in table '$sTable' has a wrong type: found '$sActualFieldType' while expecting '$sDBFieldType'";
 						} 
 						if ($bToBeChanged)
 						{
-							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs";
+							$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` CHANGE `$sField` `$sField` $sFieldSpecs";
 						}
 
 						// Create indexes (external keys only... so far)
 						//
 						if ($oAttDef->IsExternalKey() && !CMDBSource::HasIndex($sTable, $sField))
 						{
-							$aErrors[$sClass][] = "Foreign key '$sField' in table '$sTable' should have an index";
-							$aSugFix[$sClass][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
+							$aErrors[$sClass][$sAttCode][] = "Foreign key '$sField' in table '$sTable' should have an index";
+							$aSugFix[$sClass][$sAttCode][] = "ALTER TABLE `$sTable` ADD INDEX (`$sField`)";
 						}
 					}
 				}

+ 50 - 1
core/valuesetdef.class.inc.php

@@ -35,7 +35,7 @@ abstract class ValueSetDefinition
 	}
 
 
-	public function GetValues($aArgs, $sBeginsWith)
+	public function GetValues($aArgs, $sBeginsWith = '')
 	{
 		if (!$this->m_bIsLoaded)
 		{
@@ -117,6 +117,55 @@ class ValueSetObjects extends ValueSetDefinition
 
 
 /**
+ * Set of existing values for a link set attribute, given a relation code 
+ *
+ * @package     iTopORM
+ * @author      Romain Quetiez <romainquetiez@yahoo.fr>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @link        www.itop.com
+ * @since       1.0
+ * @version     $itopversion$
+ */
+class ValueSetRelatedObjects extends ValueSetDefinition
+{
+	protected $m_sRelationCode;
+	protected $m_iMaxDepth;
+//	protected $m_aOrderBy;
+
+	public function __construct($sRelationCode, $iMaxDepth = 99)
+	{
+		$this->m_sRelationCode = $sRelationCode;
+		$this->m_iMaxDepth = $iMaxDepth;
+//		$this->m_aOrderBy = $aOrderBy;
+	}
+
+	protected function LoadValues($aArgs)
+	{
+		$this->m_aValues = array();
+
+		if (!array_key_exists('this', $aArgs))
+		{
+			throw new CoreException("Missing 'this' in arguments", array('args' => $aArgs));
+		}		
+
+		$oTarget = $aArgs['this->object()'];
+
+		$oTargetNeighbors = $oTarget->GetRelatedObjects($this->m_sRelationCode, $this->m_iMaxDepth);
+		while ($oObject = $oTargetNeighbors->Fetch())
+		{
+			$this->m_aValues[$oObject->GetKey()] = $oObject->GetAsHTML($oObject->GetName());
+		}
+		return true;
+	}
+	
+	public function GetValuesDescription()
+	{
+		return 'Filter: '.$this->m_sFilterExpr;
+	}
+}
+
+
+/**
  * Fixed set values (could be hardcoded in the business model) 
  *
  * @package     iTopORM

+ 10 - 7
pages/ITopConsultant.php

@@ -254,7 +254,7 @@ function DumpDatabase()
 function printMenu($sConfigFile)
 {
 	$sClassCount = count(MetaModel::GetClasses());
-	$bHasDB = MetaModel::DBExists();
+	$bHasDB = MetaModel::DBExists(false); // no need to be complete to consider that something already exists
 	$sUrl = "?config=".urlencode($sConfigFile);
 
 	echo "<div style=\"background-color:eeeeee; padding:10px;\">\n";
@@ -345,22 +345,25 @@ function DisplayDBFormatIssues($aErrors, $aSugFix, $sRepairUrl = "", $sSQLStatem
 		echo "<div style=\"width:100%;padding:10px;background:#FFAAAA;display:;\">";
 		echo "<h1>Wrong Database format</h1>\n";
 		echo "<p>The current database is not consistent with the given business model. Please investigate.</p>\n";
-		foreach ($aErrors as $sClass => $aMessages)
+		foreach ($aErrors as $sClass => $aTarget)
 		{
 			echo "<p>Wrong declaration (or DB format ?) for class <b>$sClass</b></p>\n";
 			echo "<ul class=\"treeview\">\n";
 			$i = 0;
-			foreach ($aMessages as $sMsg)
+			foreach ($aTarget as $sTarget => $aMessages)
 			{
+				echo "<p>Wrong declaration for attribute <b>$sTarget</b></p>\n";
+				$sMsg = implode(' AND ', $aMessages);
 				if (!empty($sRepairUrl))
 				{
-					$aSQLFixes[] = $aSugFix[$sClass][$i];
-					$sUrl = "$sRepairUrl&$sSQLStatementArgName=".urlencode($aSugFix[$sClass][$i]);
-					echo "<li>$sMsg (<a href=\"$sUrl\" title=\"".$aSugFix[$sClass][$i]."\" target=\"_blank\">fix it now!</a>)</li>\n";
+					$aSQLFixes = array_merge($aSQLFixes, $aSugFix[$sClass][$sTarget]);
+					$sSQLFixes = implode('; ', $aSugFix[$sClass][$sTarget]);
+					$sUrl = "$sRepairUrl&$sSQLStatementArgName=".urlencode($sSQLFixes);
+					echo "<li>$sMsg (<a href=\"$sUrl\" title=\"".htmlentities($sSQLFixes)."\" target=\"_blank\">fix it now!</a>)</li>\n";
 				}
 				else
 				{
-					echo "<li>$sMsg ({$aSugFix[$sClass][$i]})</li>\n";
+					echo "<li>$sMsg (".htmlentities($sSQLFixes).")</li>\n";
 				}
 				$i++;
 			}

+ 2 - 2
pages/advanced_search.php

@@ -226,8 +226,8 @@ function Page3_ViewResults($oPage, $oFilter)
 		$oSet = new CMDBObjectSet($oFilter);
 		$oPage->p("Found ".$oSet->Count()." items");
 	
-		$sFilterPhrase = $oFilter->serialize();
-		$oPage->p("<a href=\"/pages/index.php?operation=direct&filter=$sFilterPhrase\">See detailed results</a>"); 
+		$sFilterPhrase = urlencode($oFilter->serialize());
+		$oPage->p("<a href=\"/pages/index.php?operation=search&filter=$sFilterPhrase\">See detailed results</a>"); 
 	}
 }
 

+ 47 - 26
pages/csvimport.php

@@ -62,7 +62,7 @@ function MakeExtFieldSelectValue($sAttCode, $sExtAttCode)
 
 function ShowTableForm($oPage, $oCSVParser, $sClass)
 {
-	$aData = $oCSVParser->ToArray(null, 3);
+	$aData = $oCSVParser->ToArray(1, null, 3);
 	$aColToRow = array();
 	foreach($aData as $aRow)
 	{
@@ -193,6 +193,10 @@ function ShowTableForm($oPage, $oCSVParser, $sClass)
 		{
 			$aFields["field$iFieldIndex"]["value"] = $aColToRow[$iFieldIndex];
 		}
+		else
+		{
+			// Houston... 		
+		}
 	}
 	$oPage->details($aFields);
 }
@@ -354,12 +358,24 @@ function Do_Welcome($oPage, $sClass)
 	$sWiztep = "1_welcome";
 	$oPage->p("<h1>Bulk load from CSV data / step 1</h1>");
 
+	// Reload values (in case we are reaching this page from the next one
 	$sCSVData = utils::ReadPostedParam('csvdata');
+	$sSep = utils::ReadPostedParam('separator', ',');
+	$sTQualif = utils::ReadPostedParam('textqualifier', '"');
+
+	$aSeparators = array(',' => ', (coma)', ';' => ';', ';' => ';', '|' => '|', '#' => '#', '@' => '@', ':' => ':');
+	$aTextQualifiers = array('"' => '"', "'" => "'", '`' => '`', '/' => '/');
 
 	$oPage->add("<form method=\"post\" action=\"\">");
 	$oPage->MakeClassesSelect("class", $sClass, 50, UR_ACTION_BULK_MODIFY);
 	$oPage->add("<br/>");
-	$oPage->add("<textarea rows=\"25\" cols=\"100\" name=\"csvdata\" wrap=\"soft\">$sCSVData</textarea>");
+	$oPage->add("<textarea rows=\"25\" cols=\"100\" name=\"csvdata\" wrap=\"soft\">".htmlentities($sCSVData)."</textarea>");
+	$oPage->add("<br/>");
+	$oPage->add("Separator: ");
+	$oPage->add_select($aSeparators, 'separator', $sSep, 50);
+	$oPage->add("<br/>");
+	$oPage->add("Text qualifier: ");
+	$oPage->add_select($aTextQualifiers, 'textqualifier', $sTQualif, 50);
 	$oPage->add("<br/>");
 	$oPage->add("<input type=\"hidden\" name=\"fromwiztep\" value=\"$sWiztep\">");
 	$oPage->add("<input type=\"submit\" name=\"todo\" value=\"Next\"><br/>\n");
@@ -415,12 +431,13 @@ function Do_Format($oPage, $sClass)
 	$sWiztep = "2_format";
 
 	$sCSVData = utils::ReadPostedParam('csvdata');
-	$oCSVParser = new CSVParser($sCSVData); 
-	$sSep = $oCSVParser->GuessSeparator();
-	$iSkip = $oCSVParser->GuessSkipLines();
+	$sSep = utils::ReadPostedParam('separator');
+	$sTQualif = utils::ReadPostedParam('textqualifier');
+	$oCSVParser = new CSVParser($sCSVData, $sSep, $sTQualif); 
+	$iSkip = 1;
 
 	// No data ?
-	$aData = $oCSVParser->ToArray(null);
+	$aData = $oCSVParser->ToArray();
 	$iTarget = count($aData);
 	if ($iTarget == 0)
 	{
@@ -429,29 +446,32 @@ function Do_Format($oPage, $sClass)
 		return;
 	}
 
-	// Guess the format :
-	$oPage->p("Guessed separator: '<strong>$sSep</strong>' (ASCII=".ord($sSep).")");
-	$oPage->p("Guessed # of lines to skip: $iSkip");
+	// Expected format - to be improved
+	$oPage->p("Separator: '<strong>$sSep</strong>'");
+	$oPage->p("Text qualifier: '<strong>$sTQualif</strong>'");
+	$oPage->p("The first line will be skipped (considered as being the list of fields)");
 
 	$oPage->p("Target: $iTarget rows");
 
-    $oPage->Add("<form method=\"post\" action=\"\">");
+	$oPage->add("<form method=\"post\" action=\"\">");
 	ShowTableForm($oPage, $oCSVParser, $sClass);
-    $oPage->Add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">");
-    $oPage->Add("<input type=\"hidden\" name=\"csvdata\" value=\"$sCSVData\">");
-    $oPage->Add("<input type=\"hidden\" name=\"separator\" value=\"$sSep\">");
-    $oPage->Add("<input type=\"hidden\" name=\"skiplines\" value=\"$iSkip\">");
-
-    $oPage->Add("<input type=\"hidden\" name=\"fromwiztep\" value=\"$sWiztep\">");
-    $oPage->add("<input type=\"submit\" name=\"todo\" value=\"Back\">");
-    $oPage->Add("<input type=\"submit\" name=\"todo\" value=\"Next\">");
-	$oPage->Add("</form>");
+	$oPage->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">");
+	$oPage->add("<input type=\"hidden\" name=\"csvdata\" value=\"".htmlentities($sCSVData)."\">");
+	$oPage->add("<input type=\"hidden\" name=\"separator\" value=\"".htmlentities($sSep)."\">");
+	$oPage->add("<input type=\"hidden\" name=\"textqualifier\" value=\"".htmlentities($sTQualif)."\">");
+	$oPage->add("<input type=\"hidden\" name=\"skiplines\" value=\"$iSkip\">");
+
+	$oPage->add("<input type=\"hidden\" name=\"fromwiztep\" value=\"$sWiztep\">");
+	$oPage->add("<input type=\"submit\" name=\"todo\" value=\"Back\">");
+	$oPage->add("<input type=\"submit\" name=\"todo\" value=\"Next\">");
+	$oPage->add("</form>");
 }
 
 function DoProcessOrVerify($oPage, $sClass, CMDBChange $oChange = null)
 {
 	$sCSVData = utils::ReadPostedParam('csvdata'); 
 	$sSep = utils::ReadPostedParam('separator');
+	$sTQualif = utils::ReadPostedParam('textqualifier');
 	$iSkip = utils::ReadPostedParam('skiplines'); 
 	$aFieldMap = utils::ReadPostedParam('fmap');
 	$aIsReconcKey = utils::ReadPostedParam('iskey');
@@ -465,16 +485,15 @@ function DoProcessOrVerify($oPage, $sClass, CMDBChange $oChange = null)
 		return;
 	}
 
-	$oCSVParser = new CSVParser($sCSVData);
-	$oCSVParser->SetSeparator($sSep);
-	$oCSVParser->SetSkipLines($iSkip);
-	$aData = $oCSVParser->ToArray(null);
+	$oCSVParser = new CSVParser($sCSVData, $sSep, $sTQualif);
+	$aData = $oCSVParser->ToArray($iSkip, null);
 	$iTarget = count($aData);
 
 	$oPage->p("<h2>Goal summary</h2>");
 	$oPage->p("Target: $iTarget rows");
 
-	$aSampleData = $oCSVParser->ToArray(array_keys($aFieldMap), 5);
+	$aSampleData = $oCSVParser->ToArray($iSkip, array_keys($aFieldMap), 5);
+
 	$aDisplayConfig = array();
 	$aExtKeys = array();
 	foreach ($aFieldMap as $sFieldId=>$sColDesc)
@@ -519,6 +538,7 @@ function DoProcessOrVerify($oPage, $sClass, CMDBChange $oChange = null)
 			$aDisplayConfig[$sFieldId] = array("label"=>"-?-?-$sColDesc-?-?-", "description"=>"");
 		}
 	}
+	
 	$oPage->table($aDisplayConfig, $aSampleData);
 
 	if ($oChange)
@@ -558,8 +578,9 @@ function DoProcessOrVerify($oPage, $sClass, CMDBChange $oChange = null)
 
 	$oPage->add("<form method=\"post\" action=\"\">");
 	$oPage->add("<input type=\"hidden\" name=\"class\" value=\"$sClass\">");
-	$oPage->add("<input type=\"hidden\" name=\"csvdata\" value=\"$sCSVData\">");
-	$oPage->add("<input type=\"hidden\" name=\"separator\" value=\"$sSep\">");
+	$oPage->add("<input type=\"hidden\" name=\"csvdata\" value=\"".htmlentities($sCSVData)."\">");
+	$oPage->add("<input type=\"hidden\" name=\"separator\" value=\"".htmlentities($sSep)."\">");
+	$oPage->add("<input type=\"hidden\" name=\"textqualifier\" value=\"".htmlentities($sTQualif)."\">");
 	$oPage->add("<input type=\"hidden\" name=\"skiplines\" value=\"$iSkip\">");
 	$oPage->add_input_hidden("fmap", $aFieldMap);
 	$oPage->add_input_hidden("iskey", $aIsReconcKey);

+ 0 - 56
pages/index.php

@@ -149,36 +149,6 @@ function DisplayChangesLog(WebPage $oPage, $sClassName, $sKey)
 	$oPage->p("<a href=\"?operation=delete&class=$sClassName&key=$sKey\">Delete this object (no confirmation!)</a>");
 }
 
-function DumpObjectsAsCSV(WebPage $oPage, $sClassName, $oSearchFilter = null,  $sSeparator = ",")
-{
-	global $oContext;
-	
-	$aHeader = array();
-	$aHeader[] = 'pkey';
-	foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode=>$oAttDef)
-	{
-		$aHeader[] = $oAttDef->GetLabel();
-	}
-	$oPage->Add(join($sSeparator, $aHeader)."\n");
-	
-	if ($oSearchFilter == null)
-	{
-		$oSearchFilter = $oContext->NewFilter($sClassName);
-	}
-	$oObjectSet = new CMDBObjectSet($oSearchFilter);
-	
-	while ($oObj = $oObjectSet->Fetch())
-	{
-		$aRow = array();
-		$aRow[] = $oObj->GetKey();
-		foreach($oObj->GetAttributesList($sClassName) as $sAttCode)
-		{
-			$aRow[] = $oObj->GetAsCSV($sAttCode);
-		}
-		$oPage->Add(join($sSeparator, $aRow)."\n");
-	}
-}
-
 function DumpObjects(WebPage $oPage, $sClassName, CMDBSearchFilter $oSearchFilter = null)
 {
 	global $oContext;
@@ -554,32 +524,6 @@ switch($operation)
         DeleteObject($oPage, $sClass, $sKey);
     break;
 
-	case 'direct':
-        $sFilter = ReadParam('filter');
-        $sFormat = ReadParam('format', 'html');
-		$oSearchFilter = CMDBSearchFilter::unserialize($sFilter);
-		switch($sFormat)
-		{
-			case 'csv':
-			$oPage->small_p($oSearchFilter->__DescribeHTML());
-			$oPage->Add("<TEXTAREA ROWS=\"30\" COLS=\"100\">");
-			DumpObjectsAsCSV($oPage, $oSearchFilter->GetClass(), $oSearchFilter);
-			$oPage->Add("</TEXTAREA>");
-			break;
-			
-			case 'xls':
-			$oPage->add_header('Content-disposition: attachment;filename=served.xls');  // Will fool Excel
-			$oPage->add_header('Content-Type: application/vnd.ms-excel');  // Will fool Excel
-			DumpObjects($oPage, $oSearchFilter->GetClass(), $oSearchFilter);
-			break;
-			
-			case 'html':
-			default:
-			$oSet = new CMDBObjectSet($oSearchFilter);
-			cmdbAbstractObject::DisplaySet($oPage, $oSet);
-		}
-	break;
-
 	case 'addlinks':
 		$sClass = ReadParam('class');
         $sKey = ReadParam('key');

+ 1682 - 1590
pages/testlist.inc.php

@@ -1,1590 +1,1682 @@
-<?php
-
-class TestSQLQuery extends TestScenarioOnDB
-{
-	static public function GetName() {return 'SQLQuery';}
-	static public function GetDescription() {return 'SQLQuery does not depend on the rest of the framework, therefore it makes sense to have a separate test framework for it';}
-
-	static public function GetDBHost() {return 'localhost';}
-	static public function GetDBUser() {return 'RomainDBLogin';}
-	static public function GetDBPwd() {return '';}
-	static public function GetDBName() {return 'TestSQLQuery';}
-	static public function GetDBSubName() {return 'taratata';}
-
-
-	protected function DoPrepare()
-	{
-		parent::DoPrepare();
-		cmdbSource::CreateTable('CREATE TABLE `myTable` (myKey INT(11) NOT NULL auto_increment, column1 VARCHAR(255), column2 VARCHAR(255), PRIMARY KEY (`myKey`)) ENGINE = innodb');
-		cmdbSource::CreateTable('CREATE TABLE `myTable1` (myKey1 INT(11) NOT NULL auto_increment, column1_1 VARCHAR(255), column1_2 VARCHAR(255), PRIMARY KEY (`myKey1`)) ENGINE = innodb');
-		cmdbSource::CreateTable('CREATE TABLE `myTable2` (myKey2 INT(11) NOT NULL auto_increment, column2_1 VARCHAR(255), column2_2 VARCHAR(255), PRIMARY KEY (`myKey2`)) ENGINE = innodb');
-	}
-
-	protected function DoExecute()
-	{
-		$oQuery = new SQLQuery(
-			$sTable = 'myTable',
-			$sTableAlias = 'myTableAlias',
-			$aFields = array('column1'=>new FieldExpression('column1', 'myTableAlias'), 'column2'=>new FieldExpression('column2', 'myTableAlias')),
-			$oCondition = new BinaryExpression(new FieldExpression('column1', 'myTableAlias'), 'LIKE', new ScalarExpression('trash')),
-			$aFullTextNeedles = array('column1'),
-			$bToDelete = false,
-			$aValues = array()
-		);
-		$oQuery->AddCondition(Expression::FromOQL('DATE(NOW() - 1200 * 2) > \'2008-07-31\''));
-
-		$oSubQuery1 = new SQLQuery(
-			$sTable = 'myTable1',
-			$sTableAlias = 'myTable1Alias',
-			$aFields = array('column1_1'=>new FieldExpression('column1', 'myTableAlias'), 'column1_2'=>new FieldExpression('column1', 'myTableAlias')),
-			$oCondition = new TrueSQLExpression,
-			$aFullTextNeedles = array(),
-			$bToDelete = false,
-			$aValues = array()
-		);
-
-		$oSubQuery2 = new SQLQuery(
-			$sTable = 'myTable2',
-			$sTableAlias = 'myTable2Alias',
-			$aFields = array('column2_1'=>new FieldExpression('column2', 'myTableAlias'), 'column2_2'=>new FieldExpression('column2', 'myTableAlias')),
-			$oCondition = new TrueSQLExpression,
-			$aFullTextNeedles = array(),
-			$bToDelete = false,
-			$aValues = array()
-		);
-
-		$oQuery->AddInnerJoin($oSubQuery1, 'column1', 'column1_1');
-		$oQuery->AddLeftJoin($oSubQuery2, 'column2', 'column2_2');
-		
-		$oQuery->DisplayHtml();
-		$oQuery->RenderDelete();
-		$oQuery->RenderUpdate();
-		echo '<p>'.$oQuery->RenderSelect().'</p>';
-		$oQuery->RenderSelect(array('column1'));
-		$oQuery->RenderSelect(array('column1', 'column2'));
-	}
-}
-
-class TestOQLParser extends TestFunction
-{
-	static public function GetName() {return 'Check OQL parsing';}
-	static public function GetDescription() {return 'Attempts a series of queries, and in particular those with a bad syntax';}
-
-	protected function CheckQuery($sQuery, $bIsCorrectQuery)
-	{
-		$oOql = new OqlInterpreter($sQuery);
-		try
-		{
-			$oTrash = $oOql->Parse(); // Not expecting a given format, otherwise use ParseExpression/ParseObjectQuery/ParseValueSetQuery
-			MyHelpers::var_dump_html($oTrash, true);
-		}
-		catch (OQLException $OqlException)
-		{
-			if ($bIsCorrectQuery)
-			{
-				echo "<p>More info on this unexpected failure:<br/>".$OqlException->getHtmlDesc()."</p>\n";
-				throw $OqlException;
-				return false;
-			}
-			else
-			{
-				// Everything is fine :-)
-				echo "<p>More info on this expected failure:<br/>".$OqlException->getHtmlDesc()."</p>\n";
-				return true;
-			}
-		}
-		// The query was correctly parsed, was it expected to be correct ?
-		if ($bIsCorrectQuery)
-		{
-			return true;
-		}
-		else
-		{
-			throw new UnitTestException("The query '$sQuery' was parsed with success, while it shouldn't (?)");
-			return false;
-		}
-	}
-
-	protected function TestQuery($sQuery, $bIsCorrectQuery)
-	{
-		if (!$this->CheckQuery($sQuery, $bIsCorrectQuery))
-		{
-			return false;
-		}
-		return true;
-	}
-
-	public function DoExecute()
-	{
-		$aQueries = array(
-			'SELECT toto' => true,
-			'SELECT toto WHERE toto.a = 1' => true,
-			'SELECT toto WHERE toto.a=1' => true,
-			'SELECT toto WHERE toto.a = "1"' => true,
-			'SELECT toto WHHHERE toto.a = "1"' => false,
-			'SELECT toto WHERE toto.a == "1"' => false,
-			'SELECT toto WHERE toto.a % 1' => false,
-			//'SELECT toto WHERE toto.a LIKE 1' => false,
-			'SELECT toto WHERE toto.a like \'arg\'' => false,
-			'SELECT toto WHERE toto.a NOT LIKE "That\'s it"' => true,
-			'SELECT toto WHERE toto.a NOT LIKE "That\'s "it""' => false,
-			'SELECT toto WHERE toto.a NOT LIKE "That\'s \\"it\\""' => true,
-			'SELECT toto WHERE toto.a NOT LIKE \'That"s it\'' => true,
-			'SELECT toto WHERE toto.a NOT LIKE \'That\'s it\'' => false,
-			'SELECT toto WHERE toto.a NOT LIKE \'That\\\'s it\'' => true,
-			'SELECT toto WHERE toto.a NOT LIKE "blah \\ truc"' => false,
-			'SELECT toto WHERE toto.a NOT LIKE "blah \\\\ truc"' => true,
-			'SELECT toto WHERE toto.a NOT LIKE \'blah \\ truc\'' => false,
-			'SELECT toto WHERE toto.a NOT LIKE \'blah \\\\ truc\'' => true,
-
-			'SELECT toto WHERE toto.a NOT LIKE "\\\\"' => true,
-			'SELECT toto WHERE toto.a NOT LIKE "\\""' => true,
-			'SELECT toto WHERE toto.a NOT LIKE "\\"\\\\"' => true,
-			'SELECT toto WHERE toto.a NOT LIKE "\\\\\\""' => true,
-			'SELECT toto WHERE toto.a NOT LIKE ""' => true,
-			'SELECT toto WHERE toto.a NOT LIKE "\\\\"' => true,
-			"SELECT UserRightsMatrixClassGrant WHERE UserRightsMatrixClassGrant.class = 'lnkContactRealObject' AND UserRightsMatrixClassGrant.action = 'modify' AND UserRightsMatrixClassGrant.login = 'Denis'" => true,
-			"SELECT A WHERE A.col1 = 'lit1' AND A.col2 = 'lit2' AND A.col3 = 'lit3'" => true,
-
-			'SELECT toto WHERE toto.a NOT LIKE "blah" AND toto.b LIKE "foo"' => true,
-
-			//'SELECT toto WHERE toto.a > \'asd\'' => false,
-			'SELECT toto WHERE toto.a = 1 AND toto.b LIKE "x" AND toto.f >= 12345' => true,
-			'SELECT Device JOIN Site ON Device.site = Site.id' => true,
-			'SELECT Device JOIN Site ON Device.site = Site.id JOIN Country ON Site.location = Country.id' => true,
-
-			"SELECT A JOIN B ON A.myB = B.id WHERE (A.col1 = 123 AND B.col1 = 'aa') OR (A.col3 = 'zzz' AND B.col4 > 100)" => true,
-			"SELECT A JOIN B ON A.myB = B.id WHERE (A.col1 = B.col2 AND B.col1 = A.col2) OR (A.col3 = '' AND B.col4 > 100)" => true,
-			"SELECT A JOIN B ON A.myB = B.id WHERE A.col1 + B.col2 * B.col1 = A.col2" => true,
-			"SELECT A JOIN B ON A.myB = B.id WHERE A.col1 + (B.col2 * B.col1) = A.col2" => true,
-			"SELECT A JOIN B ON A.myB = B.id WHERE (A.col1 + B.col2) * B.col1 = A.col2" => true,
-
-			'SELECT Device AS D_ JOIN Site AS S_ ON D_.site = S_.id WHERE S_.country = "Francia"' => true,
-		);
-
-		$iErrors = 0;
-
-		foreach($aQueries as $sQuery => $bIsCorrectQuery)
-		{
-			$sIsOk = $bIsCorrectQuery ? 'good' : 'bad';
-			echo "<h4>Testing query: $sQuery ($sIsOk)</h4>\n";
-			$bRet = $this->TestQuery($sQuery, $bIsCorrectQuery);
-			if (!$bRet) $iErrors++;
-		}
-		
-		return ($iErrors == 0);
-	}
-}
-
-
-class TestGenericItoMyModel extends TestBizModelGeneric
-{
-	static public function GetName()
-	{
-		return 'Generic RO test on '.self::GetConfigFile();
-	}
-
-	static public function GetConfigFile() {return '../config-test-mymodel.php';}
-}
-
-class TestGenericItopBigModel extends TestBizModelGeneric
-{
-	static public function GetName()
-	{
-		return 'Generic RO test on '.self::GetConfigFile();
-	}
-
-	static public function GetConfigFile() {return '../config-test-itopv06.php';}
-}
-
-class TestUserRightsMatrixItop extends TestUserRights
-{
-	static public function GetName()
-	{
-		return 'User rights test on user rights matrix';
-	}
-
-	static public function GetDescription()
-	{
-		return 'blah blah blah';
-	}
-
-	public function DoPrepare()
-	{
-		parent::DoPrepare();
-		MetaModel::Startup('../config-test-itopv06.php');
-	}
-
-	protected function DoExecute()
-	{
-		$sUser = 'Romain';
-		echo "<p>Totor: ".(UserRights::Login('Totor', 'toto') ? 'ok' : 'NO')."</p>\n";
-		echo "<p>Romain: ".(UserRights::Login('Romain', 'toto') ? 'ok' : 'NO')."</p>\n";
-		echo "<p>User: ".UserRights::GetUser()."</p>\n";
-		echo "<p>On behalf of...".UserRights::GetRealUser()."</p>\n";
-
-		echo "<p>Denis (impersonate) : ".(UserRights::Impersonate('Denis', 'tutu') ? 'ok' : 'NO')."</p>\n";
-		echo "<p>User: ".UserRights::GetUser()."</p>\n";
-		echo "<p>On behalf of...".UserRights::GetRealUser()."</p>\n";
-
-		UserRights::GetFilter('bizOrganization'); // returns a filter object
-
-		$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT bizOrganization"));
-		echo "<p>IsActionAllowed...".(UserRights::IsActionAllowed('bizOrganization', UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES ? 'ok' : 'NO')."</p>\n";
-		echo "<p>IsStimulusAllowed...".(UserRights::IsStimulusAllowed('bizOrganization', 'myStimulus', $oSet) == UR_ALLOWED_YES ? 'ok' : 'NO')."</p>\n";
-		echo "<p>IsActionAllowedOnAttribute...".(UserRights::IsActionAllowedOnAttribute('bizOrganization', 'myattribute', UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES ? 'ok' : 'NO')."</p>\n";
-		return true;
-	}
-}
-
-///////////////////////////////////////////////////////////////////////////
-// Test a complex biz model on the fly
-///////////////////////////////////////////////////////////////////////////
-
-class TestMyBizModel extends TestBizModel
-{
-	static public function GetName()
-	{
-		return 'A series of tests on a weird business model';
-	}
-
-	static public function GetDescription()
-	{
-		return 'Attempts various operations and build complex queries';
-	}
-	
-	static public function GetConfigFile() {return '../config-test-mymodel.php';}
-
-	function test_linksinfo()
-	{
-		echo "<h4>Enum links</h4>";
-		MyHelpers::var_dump_html(MetaModel::EnumReferencedClasses("cmdbTeam"));
-		MyHelpers::var_dump_html(MetaModel::EnumReferencingClasses("Organization"));
-	
-		MyHelpers::var_dump_html(MetaModel::EnumLinkingClasses());
-		MyHelpers::var_dump_html(MetaModel::EnumLinkingClasses("cmdbContact"));
-		MyHelpers::var_dump_html(MetaModel::EnumLinkingClasses("cmdWorkshop"));
-		MyHelpers::var_dump_html(MetaModel::GetLinkLabel("Liens_entre_contacts_et_workshop", "toworkshop"));
-	}
-	
-	function test_list_attributes()
-	{
-		echo "<h4>List attributes</h4>";
-		foreach(MetaModel::ListAttributeDefs("cmdbTeam") as $sAttCode=>$oAttDef)
-		{
-			echo $oAttDef->GetLabel()." / ".$oAttDef->GetDescription()." / ".$oAttDef->GetType()."</br>\n";
-		}
-	}
-	
-	function test_search()
-	{
-		echo "<h4>Two searches</h4>";
-		$oFilterAllDevs = new DBObjectSearch("cmdbTeam");
-		$oAllDevs = new DBObjectSet($oFilterAllDevs);
-		
-		echo "Found ".$oAllDevs->Count()." items.</br>\n";
-		while ($oDev = $oAllDevs->Fetch())
-		{
-			$aValues = array();
-			foreach(MetaModel::GetAttributesList($oAllDevs->GetClass()) as $sAttCode)
-			{
-				$aValues[] = MetaModel::GetLabel(get_class($oDev), $sAttCode)." (".MetaModel::GetDescription(get_class($oDev), $sAttCode).") = ".$oDev->GetAsHTML($sAttCode);
-			}
-			echo $oDev->GetKey()." => ".implode(", ", $aValues)."</br>\n";
-		}
-	
-		// a second one
-		$oMyFilter = new DBObjectSearch("cmdbContact");
-		//$oMyFilter->AddCondition("name", "aii", "Finishes with");
-		$oMyFilter->AddCondition("name", "aii");
-		$this->search_and_show_list($oMyFilter);
-		
-	}
-	
-	function test_reload()
-	{
-		echo "<h4>Reload</h4>";
-		$team = MetaModel::GetObject("cmdbContact", "2");
-		echo "Chargement de l'attribut headcount: {$team->Get("headcount")}</br>\n";
-		MyHelpers::var_dump_html($team);
-	}
-	
-	function test_setattribute()
-	{
-		echo "<h4>Set attribute and update</h4>";
-		$team = MetaModel::GetObject("cmdbTeam", "2");
-		$team->Set("headcount", rand(1,1000));
-		$team->Set("email", "Luis ".rand(9,250));
-		MyHelpers::var_dump_html($team->ListChanges());
-		echo "New headcount = {$team->Get("headcount")}</br>\n";
-		echo "Computed name = {$team->Get("name")}</br>\n";
-	
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$oMyChange->Set("userinfo", "test_setattribute / Made by robot #".rand(1,100));
-		$iChangeId = $oMyChange->DBInsert();
-	
-		//MetaModel::StartDebugQuery();
-		$team->DBUpdateTracked($oMyChange);
-		//MetaModel::StopDebugQuery();
-	
-		echo "<h4>Check the modified team</h4>";
-		$oTeam = MetaModel::GetObject("cmdbTeam", "2");
-		MyHelpers::var_dump_html($oTeam);
-	}
-	function test_newobject()
-	{
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$oMyChange->Set("userinfo", "test_newobject / Made by robot #".rand(1,100));
-		$iChangeId = $oMyChange->DBInsert();
-	
-		echo "<h4>Create a new object (team)</h4>";
-		$oNewTeam = MetaModel::NewObject("cmdbTeam");
-		$oNewTeam->Set("name", "ekip2choc #".rand(1000, 2000));
-		$oNewTeam->Set("email", "machin".rand(1,100)."@tnut.com");
-		$oNewTeam->Set("email", null);
-		$oNewTeam->Set("owner", "ITOP");
-		$oNewTeam->Set("headcount", "0".rand(38000, 38999)); // should be reset to an int value
-		$iId = $oNewTeam->DBInsertTracked($oMyChange);
-		echo "Created new team: $iId</br>";
-		echo "<h4>Delete team #$iId</h4>";
-		$oTeam = MetaModel::GetObject("cmdbTeam", $iId);
-		$oTeam->DBDeleteTracked($oMyChange);
-		echo "Deleted team: $iId</br>";
-		MyHelpers::var_dump_html($oTeam);
-	}
-	
-	
-	function test_updatecolumn()
-	{
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$oMyChange->Set("userinfo", "test_updatecolumn / Made by robot #".rand(1,100));
-		$iChangeId = $oMyChange->DBInsert();
-	
-		$sNewEmail = "updatecol".rand(9,250)."@quedlaballe.com";
-		echo "<h4>Update a the email: set to '$sNewEmail'</h4>";
-		$oMyFilter = new DBObjectSearch("cmdbContact");
-		$oMyFilter->AddCondition("name", "o", "Contains");
-	
-		echo "Candidates before:</br>";
-		$this->search_and_show_list($oMyFilter);
-	
-		MetaModel::BulkUpdateTracked($oMyChange, $oMyFilter, array("email" => $sNewEmail));
-	
-		echo "Candidates after:</br>";
-		$this->search_and_show_list($oMyFilter);
-	}
-	
-	function test_error()
-	{
-		trigger_error("Stop requested", E_USER_ERROR);
-	}
-	
-	function test_changetracking()
-	{
-		echo "<h4>Create a change</h4>";
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$oMyChange->Set("userinfo", "Made by robot #".rand(1,100));
-		$iChangeId = $oMyChange->DBInsert();
-		echo "Created new change: $iChangeId</br>";
-		MyHelpers::var_dump_html($oMyChange);
-	
-		echo "<h4>Create a new object (team)</h4>";
-		$oNewTeam = MetaModel::NewObject("cmdbTeam");
-		$oNewTeam->Set("name", "ekip2choc #".rand(1000, 2000));
-		$oNewTeam->Set("email", "machin".rand(1,100)."@tnut.com");
-		$oNewTeam->Set("email", null);
-		$oNewTeam->Set("owner", "ITOP");
-		$oNewTeam->Set("headcount", "0".rand(38000, 38999)); // should be reset to an int value
-		$iId = $oNewTeam->DBInsertTracked($oMyChange);
-		echo "Created new team: $iId</br>";
-		echo "<h4>Delete team #$iId</h4>";
-		$oTeam = MetaModel::GetObject("cmdbTeam", $iId);
-		$oTeam->DBDeleteTracked($oMyChange);
-		echo "Deleted team: $iId</br>";
-		MyHelpers::var_dump_html($oTeam);
-	}
-	
-	function test_zlist()
-	{
-		echo "<h4>Test ZLists</h4>";
-		$aZLists = MetaModel::EnumZLists();
-		foreach ($aZLists as $sListCode)
-		{
-			$aListInfos = MetaModel::GetZListInfo($sListCode);
-			echo "<h4>List '".$sListCode."' (".$aListInfos["description"].") of type '".$aListInfos["type"]."'</h5>\n";
-	
-			foreach (MetaModel::GetSubclasses("cmdbObjectHomeMade") as $sKlass)
-			{
-				$aItems = MetaModel::GetZListItems($sKlass, $sListCode);
-				if (count($aItems) == 0) continue;
-	
-				echo "$sKlass - $sListCode : {".implode(", ", $aItems)."}</br>\n";
-			}
-		}
-	
-		echo "<h4>IsAttributeInZList()... </h4>";
-		echo "Liens_entre_contacts_et_workshop::ws_info in list1 ? ".(MetaModel::IsAttributeInZList("Liens_entre_contacts_et_workshop", "list1", "ws_info") ? "yes" : "no")."</br>\n";
-		echo "Liens_entre_contacts_et_workshop::toworkshop in list1 ? ".(MetaModel::IsAttributeInZList("Liens_entre_contacts_et_workshop", "list1", "toworkshop") ? "yes" : "no")."</br>\n";
-	
-	}
-	
-	function test_SibuSQL()
-	{
-		echo "<h4>Simple But Structured Query Language</h4>";
-	
-		$oMyFilter = new DBObjectSearch("cmdbContact");
-		echo "Tous les contacts: ".$oMyFilter->ToSibuSQL()."<br/>\n";
-		$oNewFilter = DBObjectSearch::FromSibuSQL($oMyFilter->ToSibuSQL());
-		echo "En passant par un filtre, ca revient en : ".$oNewFilter->ToSibuSQL()."</br>\n";
-		$this->search_and_show_list($oNewFilter);
-	
-		$sFilterDesc = "cmdbContact: name Begins with '$[debutnom:as:debut du nom]' AND ownername NotLike $[ddd::]"; 
-		echo "Construction d'un filtre a partir de sa description en SibuSQL: $sFilterDesc<br/>\n";
-	
-		MyHelpers::var_dump_html(DBObjectSearch::ListSibusQLParams($sFilterDesc));
-		$oNewFilter = DBObjectSearch::FromSibuSQL($sFilterDesc, array('ddd'=>123));
-		echo "Ca revient en: ".$oNewFilter->ToSibuSQL();
-	}
-	
-	function test_pkey()
-	{
-		echo "<h4>Test search on pkey</h4>";
-		$sExpr1 = "cmdbContact: pkey IN {40, 42}";
-		$sExpr2 = "cmdbContact: pkey NOTIN {40, 42}";
-		$this->search_and_show_list_from_sibusql($sExpr1);
-		$this->search_and_show_list_from_sibusql($sExpr2);
-	
-		echo "Et maintenant, on fusionne....</br>\n";
-		$oSet1 = new CMDBObjectSet(DBObjectSearch::FromSibuSQL($sExpr1));
-		$oSet2 = new CMDBObjectSet(DBObjectSearch::FromSibuSQL($sExpr2));
-		$oIntersect = $oSet1->CreateIntersect($oSet2);
-		$oDelta = $oSet1->CreateDelta($oSet2);
-	
-		$oMerge = clone $oSet1;
-		$oMerge->Merge($oSet2);
-		$oMerge->Merge($oSet2);
-	
-		echo "Set1 - Found ".$oSet1->Count()." items.</br>\n";
-		echo "Set2 - Found ".$oSet2->Count()." items.</br>\n";
-		echo "Intersect - Found ".$oIntersect->Count()." items.</br>\n";
-		echo "Delta - Found ".$oDelta->Count()." items.</br>\n";
-		echo "Merge - Found ".$oMerge->Count()." items.</br>\n";
-		//$this->show_list($oObjSet);
-	}
-	
-	function test_relations()
-	{
-		echo "<h4>Test relations</h4>";
-		
-		//MyHelpers::var_dump_html(MetaModel::EnumRelationQueries("cmdbObjectHomeMade", "Potes"));
-		MyHelpers::var_dump_html(MetaModel::EnumRelationQueries("cmdbContact", "Potes"));
-	
-		$iMaxDepth = 9;
-		echo "Max depth = $iMaxDepth</br>\n";
-	
-		$oObj = MetaModel::GetObject("cmdbContact", 18);
-		$aRels = $oObj->GetRelatedObjects("Potes", $iMaxDepth);
-		echo $oObj->Get('name')." has some 'Potes'...</br>\n";
-		foreach ($aRels as $sClass => $aObjs)
-		{
-			echo "$sClass, count = ".count($aObjs)." =&gt; ".implode(', ', array_keys($aObjs))."</br>\n";
-			$oObjectSet = CMDBObjectSet::FromArray($sClass, $aObjs);
-			$this->show_list($oObjectSet);
-		}
-	
-		echo "<h4>Test relations - same results, by the mean of a SibuSQL</h4>";
-		$this->search_and_show_list_from_sibusql("cmdbContact: RELATED (Potes, $iMaxDepth) TO (cmdbContact: pkey = 18)");
-		
-	}
-	
-	function test_linkedset()
-	{
-		echo "<h4>Linked set attributes</h4>\n";
-		$oObj = MetaModel::GetObject("cmdbContact", 18);
-		
-		echo "<h5>Current workshops</h5>\n";
-		$oSetWorkshopsCurr = $oObj->Get("myworkshops");
-		$this->show_list($oSetWorkshopsCurr);
-	
-		echo "<h5>Setting workshops</h5>\n";
-		$oNewLink = new cmdbLiens();
-		$oNewLink->Set('toworkshop', 2);
-		$oNewLink->Set('function', 'mafonctioooon');
-		$oNewLink->Set('a1', 'tralala1');
-		$oNewLink->Set('a2', 'F7M');
-		$oSetWorkshops = CMDBObjectSet::FromArray("cmdbLiens", array($oNewLink));
-		$oObj->Set("myworkshops", $oSetWorkshops); 
-		$this->show_list($oSetWorkshops);
-	
-		echo "<h5>New workshops</h5>\n";
-		$oSetWorkshopsCurr = $oObj->Get("myworkshops");
-		$this->show_list($oSetWorkshopsCurr);
-	
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$oMyChange->Set("userinfo", "test_linkedset / Made by robot #".rand(1,100));
-		$iChangeId = $oMyChange->DBInsert();
-		$oObj->DBUpdateTracked($oMyChange);
-		$oObj = MetaModel::GetObject("cmdbContact", 18);
-	
-		echo "<h5>After the write</h5>\n";
-		$oSetWorkshopsCurr = $oObj->Get("myworkshops");
-		$this->show_list($oSetWorkshopsCurr);
-	}
-	
-	function test_object_lifecycle()
-	{
-		echo "<h4>Test object lifecycle</h4>";
-	
-	
-		MyHelpers::var_dump_html(MetaModel::GetStateAttributeCode("cmdbContact"));
-		MyHelpers::var_dump_html(MetaModel::EnumStates("cmdbContact"));
-		MyHelpers::var_dump_html(MetaModel::EnumStimuli("cmdbContact"));
-		foreach(MetaModel::EnumStates("cmdbContact") as $sStateCode => $aStateDef)
-		{
-			echo "<p>Transition from <strong>$sStateCode</strong></p>\n";
-			MyHelpers::var_dump_html(MetaModel::EnumTransitions("cmdbContact", $sStateCode));
-		}
-	
-		$oObj = MetaModel::GetObject("cmdbContact", 18);
-		echo "Current state: ".$oObj->GetState()."... let's go to school...";
-		MyHelpers::var_dump_html($oObj->EnumTransitions());
-		$oObj->ApplyStimulus("toschool");
-		echo "New state: ".$oObj->GetState()."... let's get older...";
-		MyHelpers::var_dump_html($oObj->EnumTransitions());
-		$oObj->ApplyStimulus("raise");
-		echo "New state: ".$oObj->GetState()."... let's try to go further... (should give an error)";
-		MyHelpers::var_dump_html($oObj->EnumTransitions());
-		$oObj->ApplyStimulus("raise"); // should give an error
-	}
-
-
-	protected function DoExecute()
-	{
-//				$this->ReportError("Found two different SibuSQL expression out of the (same?) filter: <em>$sExpr1</em> != <em>$sExpr2</em>");
-//			$this->ReportSuccess('Found '.$oSet->Count()." objects of class $sClassName");
-		//$this->test_linksinfo();
-		//$this->test_list_attributes();
-		//$this->test_search();
-		//$this->test_reload();
-		//$this->test_newobject();
-		$this->test_setattribute();
-		//$this->test_updatecolumn();
-		//$this->test_error();
-		//$this->test_changetracking();
-		$this->test_zlist();
-		$this->test_SibuSQL();
-		//$this->test_pkey();
-		$this->test_relations();
-		$this->test_linkedset();
-		$this->test_object_lifecycle();
-	}
-}
-
-
-///////////////////////////////////////////////////////////////////////////
-// Test a complex biz model on the fly
-///////////////////////////////////////////////////////////////////////////
-
-abstract class MyFarm extends TestBizModel
-{
-	static public function GetConfigFile() {return '../config-test-farm.php';}
-
-	protected function DoPrepare()
-	{
-		parent::DoPrepare();
-		$this->ResetDB();
-		MetaModel::DBCheckIntegrity();
-	}
-
-	protected function InsertMammal($sSpecies, $sSex, $iSpeed, $iMotherid, $iFatherId, $sName, $iHeight, $sBirth)
-	{
-		$oNew = MetaModel::NewObject('Mammal');
-		$oNew->Set('species', $sSpecies);
-		$oNew->Set('sex', $sSex);
-		$oNew->Set('speed', $iSpeed);
-		$oNew->Set('mother', $iMotherid);
-		$oNew->Set('father', $iFatherId);
-		$oNew->Set('name', $sName);
-		$oNew->Set('height', $iHeight);
-		$oNew->Set('birth', $sBirth);
-		return $this->ObjectToDB($oNew);
-	}
-
-	protected function InsertBird($sSpecies, $sSex, $iSpeed, $iMotherid, $iFatherId)
-	{
-		$oNew = MetaModel::NewObject('Bird');
-		$oNew->Set('species', $sSpecies);
-		$oNew->Set('sex', $sSex);
-		$oNew->Set('speed', $iSpeed);
-		$oNew->Set('mother', $iMotherid);
-		$oNew->Set('father', $iFatherId);
-		return $this->ObjectToDB($oNew);
-	}
-
-	protected function InsertFlyingBird($sSpecies, $sSex, $iSpeed, $iMotherid, $iFatherId, $iFlyingSpeed)
-	{
-		$oNew = MetaModel::NewObject('FlyingBird');
-		$oNew->Set('species', $sSpecies);
-		$oNew->Set('sex', $sSex);
-		$oNew->Set('speed', $iSpeed);
-		$oNew->Set('mother', $iMotherid);
-		$oNew->Set('father', $iFatherId);
-		$oNew->Set('flyingspeed', $iFlyingSpeed);
-		return $this->ObjectToDB($oNew);
-	}
-
-	private function InsertGroup($sName, $iLeaderId)
-	{
-		$oNew = MetaModel::NewObject('Group');
-		$oNew->Set('name', $sName);
-		$oNew->Set('leader', $iLeaderId);
-		$iId = $oNew->DBInsertNoReload();
-		return $iId;
-	}
-}
-
-
-class TestQueriesOnFarm extends MyFarm
-{
-	static public function GetName()
-	{
-		return 'Farm test';
-	}
-
-	static public function GetDescription()
-	{
-		return 'A series of tests on the farm business model (SQL generation)';
-	}
-
-	protected function CheckQuery($sQuery, $bIsCorrectQuery)
-	{
-		if ($bIsCorrectQuery)
-		{
-			echo "<h4 style=\"color:green;\">$sQuery</h4>\n";
-		}
-		else
-		{
-			echo "<h4 style=\"color:red;\">$sQuery</h3>\n";
-		}
-		try
-		{
-			//$oOql = new OqlInterpreter($sQuery);
-			//$oTrash = $oOql->ParseObjectQuery();
-			//MyHelpers::var_dump_html($oTrash, true);
-			$oMyFilter = DBObjectSearch::FromOQL($sQuery);
-		}
-		catch (OQLException $oOqlException)
-		{
-			if ($bIsCorrectQuery)
-			{
-				echo "<p>More info on this unexpected failure:<br/>".$oOqlException->getHtmlDesc()."</p>\n";
-				throw $oOqlException;
-				return false;
-			}
-			else
-			{
-				// Everything is fine :-)
-				echo "<p>More info on this expected failure:\n";
-				echo "<ul>\n";
-				echo "<li>".get_class($oOqlException)."</li>\n";
-				echo "<li>".$oOqlException->getMessage()."</li>\n";
-				echo "<li>".$oOqlException->getHtmlDesc()."</li>\n";
-				echo "</ul>\n";
-				echo "</p>\n";
-				return true;
-			}
-		}
-		// The query was correctly parsed, was it expected to be correct ?
-		if (!$bIsCorrectQuery)
-		{
-			throw new UnitTestException("The query '$sQuery' was parsed with success, while it shouldn't (?)");
-			return false;
-		}
-		echo "<p>To OQL: ".$oMyFilter->ToOQL()."</p>";
-
-		$this->search_and_show_list($oMyFilter);
-		
-		//echo "<p>first pass<p>\n";
-		//MyHelpers::var_dump_html($oMyFilter, true);
-		$sQuery1 = MetaModel::MakeSelectQuery($oMyFilter);
-		//echo "<p>second pass<p>\n";
-		//MyHelpers::var_dump_html($oMyFilter, true);
-		//$sQuery1 = MetaModel::MakeSelectQuery($oMyFilter);
-		
-		$sSerialize = $oMyFilter->serialize();
-		echo "<p>Serialized:$sSerialize</p>\n";
-		$oFilter2 = DBObjectSearch::unserialize($sSerialize);
-		try
-		{
-			$sQuery2 = MetaModel::MakeSelectQuery($oFilter2);
-		}
-		catch (Exception $e)
-		{
-			echo "<p>Could not compute the query after unserialize</p>\n";
-			echo "<p>Query 1: $sQuery1</p>\n";
-			MyHelpers::var_cmp_html($oMyFilter, $oFilter2);
-			throw $e;
-		}
-		//if ($oFilter2 != $oMyFilter) no, they may differ while the resulting query is the same!
-		if ($sQuery1 != $sQuery2)
-		{
-			echo "<p>serialize/unserialize mismatch :-(</p>\n";
-			MyHelpers::var_cmp_html($sQuery1, $sQuery2);
-			MyHelpers::var_cmp_html($oMyFilter, $oFilter2);
-			return false;
-		}
-		return true;
-	}
-
-	protected function DoExecute()
-	{
-//			$this->ReportError("Found two different SibuSQL expression out of the (same?) filter: <em>$sExpr1</em> != <em>$sExpr2</em>");
-//			$this->ReportSuccess('Found '.$oSet->Count()." objects of class $sClassName");
-		echo "<h3>Create protagonists...</h3>";
-
-		$iId1 = $this->InsertMammal('human', 'male', 10, 0, 0, 'romanoff', 192, '1971-07-19');
-		$iId2 = $this->InsertMammal('human', 'female', 9, 0, 0, 'rouanita', 165, '1983-01-23');
-		$this->InsertMammal('human', 'female', 3, $iId2, $iId1, 'pomme', 169, '2008-02-23');
-		$this->InsertMammal('pig', 'female', 3, 0, 0, 'grouinkette', 85, '2006-06-01');
-		$this->InsertMammal('donkey', 'female', 3, 0, 0, 'muleta', 124, '2003-11-11');
-
-		$this->InsertBird('rooster', 'male', 12, 0, 0);
-		$this->InsertFlyingBird('pie', 'female', 11, 0, 0, 35);
-
-		// Benchmarking
-		//
-		if (false)
-		{
-			define ('COUNT_BENCHMARK', 10);
-			echo "<h3>Parsing a long query, ".COUNT_BENCHMARK." times</h3>";
-			$sQuery = "SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.birth < DATE_SUB(CURRENT_DATE(), INTERVAL 10 YEAR) AND Dad.height * 2 <= ROUND(TO_DAYS(Dad.birth) / (3 + 1) * 5 - 3)";
-	
-			$fStart = MyHelpers::getmicrotime();
-			for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
-			{
-				$oMyFilter = DBObjectSearch::FromOQL($sQuery);
-			}
-			$fDuration = MyHelpers::getmicrotime() - $fStart;
-			$fParsingDuration = $fDuration / COUNT_BENCHMARK;
-			echo "<p>Mean time by op: $fParsingDuration</p>";
-		}
-
-		echo "<h3>Test queries...</h3>";
-
-		$aQueries = array(
-			'SELECT Animal' => true,
-			'SELECT Animal WHERE Animal.pkey = 1' => false,
-			'SELECT Animal WHERE Animal.id = 1' => true,
-			'SELECT Aniiimal' => false,
-			'SELECTe Animal' => false,
-			'SELECT * FROM Animal' => false,
-			'SELECT Animal AS zoo WHERE zoo.species = \'human\'' => true,
-			'SELECT Animal AS zoo WHERE species = \'human\'' => true,
-			'SELECT Animal AS zoo WHERE espece = \'human\'' => false,
-			'SELECT Animal AS zoo WHERE zoo.species IN (\'human\', "pig")' => true,
-			'SELECT Animal AS zoo WHERE CONCATENATION(zoo.species, zoo.sex) LIKE "hum%male"' => false,
-			'SELECT Animal AS zoo WHERE CONCAT(zoo.species, zoo.sex) LIKE "hum%male"' => true,
-			'SELECT Animal AS zoo WHERE zoo.species NOT IN (\'human\', "pig")' => true,
-			'SELECT Animal AS zoo WHERE zoo.kind = \'human\'' => false,
-			'SELECT Animal WHERE Animal.species = \'human\' AND Animal.sex = \'female\'' => true,
-			'SELECT Mammal AS x WHERE (x.species = \'human\' AND x.name LIKE \'ro%\') OR (x.species = \'donkey\' AND x.name LIKE \'po%\')' => true,
-			'SELECT Mammal AS x WHERE x.species = \'human\' AND x.name LIKE \'ro%\' OR x.species = \'donkey\' AND x.name LIKE \'po%\'' => true,
-			'SELECT Mammal AS m WHERE MONTH(m.birth) = 7' => true,
-			'SELECT Mammal AS m WHERE DAY(m.birth) = 19' => true,
-			'SELECT Mammal AS m WHERE YEAR(m.birth) = 1971' => true,
-			'SELECT Mammal AS m WHERE m.birth < DATE_SUB(CURRENT_DATE(), INTERVAL 10 YEAR)' => true,
-			'SELECT Mammal AS m WHERE m.birth > DATE_SUB(NOW(), INTERVAL 2000 DAY)' => true,
-			'SELECT Mammal AS m WHERE (TO_DAYS(NOW()) - TO_DAYS(m.birth)) > 2000' => true,
-			'SELECT Mammal AS m WHERE m.name = IF(FLOOR(ROUND(m.height)) > 2, "pomme", "romain")' => true,
-			'SELECT Mammal AS m WHERE (1 + 2' => false,
-			'SELECT Mammal AS m WHERE (1 + 2 * 4 / 23) > 0' => true,
-			'SELECT Mammal AS m WHERE (4 / 23 * 2 + 1) > 0' => true,
-			'SELECT Mammal AS m WHERE 1/0' => true,
-			'SELECT Mammal AS m WHERE MONTH(m.birth) = 7' => true,
-			'SELECT Animal JOIN Group ON Group.leader = Animal.id' => true,
-			'SELECT Group JOIN Animal ON Group.leader = Animal.id' => true,
-			'SELECT Animal AS A JOIN Group AS G1 ON G1.leader = A.id' => true,
-			'SELECT Animal AS A JOIN Group AS G ON FooClass.leader = A.id' => false,
-			'SELECT Animal AS A JOIN Group AS G ON G.leader = FooClass.id' => false,
-			'SELECT Animal AS A JOIN Group AS G ON G.masterchief = A.id' => false,
-			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.pkey' => false,
-			'SELECT Animal AS A JOIN Group AS G ON A.id = G.leader' => false,
-			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE A.sex=\'male\' OR G.qwerty = 123' => false,
-			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE A.sex=\'male\' OR G.name LIKE "a%"' => true,
-			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE A.id = 1' => true,
-			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE id = 1' => false,
-			'SELECT Animal AS A JOIN Group AS G ON A.member = G.id' => false,
-			'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id' => true,
-			'SELECT Mammal AS M JOIN Group AS G ON A.member = G.id' => false,
-			'SELECT Mammal AS myAlias JOIN Group AS myAlias ON myAlias.member = myAlias.id' => false,
-			'SELECT Mammal AS Mammal JOIN Group AS Mammal ON Mammal.member = Mammal.id' => false,
-			'SELECT Group AS G WHERE G.leader_name LIKE "%"' => true,
-			'SELECT Group AS G WHERE G.leader_speed < 100000' => true,
-			'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id WHERE G.leader_name LIKE "%"' => true,
-			'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id WHERE G.leader_speed < 100000' => true,
-			'SELECT Mammal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id' => true,
-			'SELECT Mammal AS Child JOIN Animal AS Dad ON Child.father = Dad.id' => true,
-			'SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id' => true,
-			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id' => true,
-			'SELECT Animal AS Dad JOIN Animal AS Child ON Child.father = Dad.id' => true,
-			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id' => true,
-			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.id = 1' => true,
-			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.name = \'romanoff\'' => false,
-			'SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id' => true,
-			'SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.name = \'romanoff\' OR Mum.speed = 0' => true,
-			'SELECT Animal AS Dad JOIN Animal AS Child ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id' => true,
-			'SELECT Mammal AS Dad JOIN Mammal AS Child ON Child.father = Dad.id' => true,
-			'SELECT Mammal AS Dad JOIN Mammal AS Child ON Child.father = Dad.id JOIN Mammal AS Mum ON Child.mother = Mum.id WHERE Dad.name = \'romanoff\' OR Mum.name=\'chloe\' OR Child.name=\'bizounours\'' => true,
-		);
-		//$aQueries = array(
-		//	'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id WHERE G.leader_name LIKE "%"' => true,
-		//);
-		foreach($aQueries as $sQuery => $bIsCorrect)
-		{
-			$this->CheckQuery($sQuery, $bIsCorrect);
-		}
-		return true;
-	}
-}
-
-
-///////////////////////////////////////////////////////////////////////////
-// Test data load
-///////////////////////////////////////////////////////////////////////////
-
-class TestBulkChangeOnFarm extends TestBizModel
-{
-	static public function GetName()
-	{
-		return 'Farm test - data load';
-	}
-
-	static public function GetDescription()
-	{
-		return 'Bulk load';
-	}
-	
-	static public function GetConfigFile() {return '../config-test-farm.php';}
-
-	protected function DoPrepare()
-	{
-		parent::DoPrepare();
-		$this->ResetDB();
-		MetaModel::DBCheckIntegrity();
-	}
-
-	protected function DoExecute()
-	{
-//			$this->ReportError("Found two different SibuSQL expression out of the (same?) filter: <em>$sExpr1</em> != <em>$sExpr2</em>");
-//			$this->ReportSuccess('Found '.$oSet->Count()." objects of class $sClassName");
-
-		$oParser = new CSVParser("#denomination,hauteur,age
-		suzy,123,2009-01-01
-		chita,456,
-		");
-		$oParser->SetSeparator(',');
-		$aData = $oParser->ToArray(array('_name', '_height', '_birth'));
-		MyHelpers::var_dump_html($aData);
-
-		$oBulk = new BulkChange(
-			'Mammal',
-			$aData,
-			array('name' => '_name', 'height' => '_height', 'birth' => '_birth'),
-			array('name'),
-			array()
-		);
-
-		$oMyChange = MetaModel::NewObject("CMDBChange");
-		$oMyChange->Set("date", time());
-		$oMyChange->Set("userinfo", "Testor");
-		$iChangeId = $oMyChange->DBInsert();
-//		echo "Created new change: $iChangeId</br>";
-
-		echo "<h3>Planned for loading...</h3>";
-		$aRes = $oBulk->Process();
-		print_r($aRes);
-		echo "<h3>Go for loading...</h3>";
-		$aRes = $oBulk->Process($oMyChange);
-		print_r($aRes);
-
-		return;
-
-		$oRawData = array(
-			'Mammal',
-			array('species', 'sex', 'speed', 'mother', 'father', 'name', 'height', 'birth'),
-			"human,male,23,0,0,romulus,192,1971
-			human,male,23,0,0,remus,154,-50
-			human,male,23,0,0,julius,160,-49
-			human,female,23,0,0,cleopatra,142,-50
-			pig,female,23,0,0,confucius,50,2003"
-		);
-	}
-}
-
-
-///////////////////////////////////////////////////////////////////////////
-// Test data load
-///////////////////////////////////////////////////////////////////////////
-
-class TestFullTextSearchOnFarm extends MyFarm
-{
-	static public function GetName()
-	{
-		return 'Farm test - full text search';
-	}
-
-	static public function GetDescription()
-	{
-		return 'Focus on the full text search feature';
-	}
-	
-	protected function DoExecute()
-	{
-		echo "<h3>Create protagonists...</h3>";
-
-		$iId1 = $this->InsertMammal('human', 'male', 10, 0, 0, 'romanoff', 192, '1971-07-19');
-		$iId2 = $this->InsertMammal('human', 'female', 9, 0, 0, 'rouanita', 165, '1983-01-23');
-		$this->InsertMammal('human', 'female', 3, $iId2, $iId1, 'pomme', 169, '2008-02-23');
-		$this->InsertMammal('pig', 'female', 3, 0, 0, 'grouinkette', 85, '2006-06-01');
-		$this->InsertMammal('donkey', 'female', 3, 0, 0, 'muleta', 124, '2003-11-11');
-
-		$this->InsertBird('rooster', 'male', 12, 0, 0);
-		$this->InsertFlyingBird('pie', 'female', 11, 0, 0, 35);
-
-		echo "<h3>Search...</h3>";
-		$oSearch = new DBObjectSearch('Mammal');
-		$oSearch->AddCondition_FullText('manof');
-		//$oResultSet = new DBObjectSet($oSearch);
-		$this->search_and_show_list($oSearch);
-	}
-}
-
-
-///////////////////////////////////////////////////////////////////////////
-// Benchmark queries
-///////////////////////////////////////////////////////////////////////////
-
-class TestItopEfficiency extends TestBizModel
-{
-	static public function GetName()
-	{
-		return 'Itop - benchmark';
-	}
-
-	static public function GetDescription()
-	{
-		return 'Measure time to perform the queries';
-	}
-
-	static public function GetConfigFile() {return '../config-itop.php';}
-
-	protected function DoBenchmark($sOqlQuery)
-	{
-		echo "<h3>Testing query: $sOqlQuery</h3>";
-
-		$fStart = MyHelpers::getmicrotime();
-		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
-		{
-			$oFilter = DBObjectSearch::FromOQL($sOqlQuery);
-		}
-		$fDuration = MyHelpers::getmicrotime() - $fStart;
-		$fParsingDuration = $fDuration / COUNT_BENCHMARK;
-
-		$fStart = MyHelpers::getmicrotime();
-		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
-		{
-			$sSQL = MetaModel::MakeSelectQuery($oFilter);
-		}
-		$fDuration = MyHelpers::getmicrotime() - $fStart;
-		$fBuildDuration = $fDuration / COUNT_BENCHMARK;
-
-		$fStart = MyHelpers::getmicrotime();
-		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
-		{
-			$res = CMDBSource::Query($sSQL);
-		}
-		$fDuration = MyHelpers::getmicrotime() - $fStart;
-		$fQueryDuration = $fDuration / COUNT_BENCHMARK;
-
-		// The fetch could not be repeated with the same results
-		// But we've seen so far that is was very very quick to exec
-		// So it makes sense to benchmark it a single time
-		$fStart = MyHelpers::getmicrotime();
-		$aRow = CMDBSource::FetchArray($res);
-		$fDuration = MyHelpers::getmicrotime() - $fStart;
-		$fFetchDuration = $fDuration;
-
-		$fStart = MyHelpers::getmicrotime();
-		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
-		{
-			$sOql = $oFilter->ToOQL();
-		}
-		$fDuration = MyHelpers::getmicrotime() - $fStart;
-		$fToOqlDuration = $fDuration / COUNT_BENCHMARK;
-
-		echo "<ul>\n";
-		echo "<li>Parsing: $fParsingDuration</li>\n";
-		echo "<li>Build: $fBuildDuration</li>\n";
-		echo "<li>Query: $fQueryDuration</li>\n";
-		echo "<li>Fetch: $fFetchDuration</li>\n";
-		echo "<li>ToOql: $fToOqlDuration</li>\n";
-		echo "</ul>\n";
-
-		// Everything but the ToOQL (wich is interesting, anyhow)
-		$fTotal = $fParsingDuration + $fBuildDuration + $fQueryDuration + $fFetchDuration; 
-
-		return array(
-			'rows' => CMDBSource::NbRows($res),
-			'duration (s)' => round($fTotal, 4),
-			'parsing (%)' => round(100 * $fParsingDuration / $fTotal, 1),
-			'build SQL (%)' => round(100 * $fBuildDuration / $fTotal, 1),
-			'query exec (%)' => round(100 * $fQueryDuration / $fTotal, 1),
-			'fetch (%)' => round(100 * $fFetchDuration / $fTotal, 1),
-			'to OQL (%)' => round(100 * $fToOqlDuration / $fTotal, 1),
-			'parsing+build (%)' => round(100 * ($fParsingDuration + $fBuildDuration) / $fTotal, 1),
-		);
-	}
-	
-	protected function DoExecute()
-	{
-		define ('COUNT_BENCHMARK', 3);
-		echo "<p>The test will be repeated ".COUNT_BENCHMARK." times</p>";
-
-		$aQueries = array(
-			'SELECT CMDBChangeOpSetAttribute',
-			'SELECT CMDBChangeOpSetAttribute WHERE id=10',
-			'SELECT CMDBChangeOpSetAttribute WHERE id=123456789',
-			'SELECT CMDBChangeOpSetAttribute WHERE CMDBChangeOpSetAttribute.id=10',
-			'SELECT bizIncidentTicket',
-			'SELECT bizIncidentTicket WHERE id=1',
-			'SELECT bizPerson',
-			'SELECT bizPerson WHERE id=1',
-			'SELECT bizIncidentTicket JOIN bizPerson ON bizIncidentTicket.agent_id = bizPerson.id WHERE bizPerson.id = 5',
-		);
-		$aStats  = array();
-		foreach ($aQueries as $sOQL)
-		{
-			$aStats[$sOQL] = $this->DoBenchmark($sOQL);
-		}
-
-		$aData = array();
-		foreach ($aStats as $sOQL => $aResults)
-		{
-			$aValues = array();
-			$aValues['OQL'] = htmlentities($sOQL);
-
-			foreach($aResults as $sDesc => $sInfo)
-			{
-				$aValues[$sDesc] = htmlentities($sInfo);
-			}
-			$aData[] = $aValues;
-		}
-		echo MyHelpers::make_table_from_assoc_array($aData);
-	}
-}
-
-///////////////////////////////////////////////////////////////////////////
-// Test data load
-///////////////////////////////////////////////////////////////////////////
-
-class TestItopWebServices extends TestWebServices
-{
-	static public function GetName()
-	{
-		return 'Itop - web services';
-	}
-
-	static public function GetDescription()
-	{
-		return 'Bulk load and ???';
-	}
-
-	protected function DoExecSingleLoad($aLoadSpec)
-	{
-		$sTitle = 'Load: '.$aLoadSpec['class'];
-		$sClass = $aLoadSpec['class'];
-		$sCsvData = $aLoadSpec['csvdata'];
-
-		$aPostData = array('class' => $sClass, 'csvdata' => $sCsvData);
-		$sRes = self::DoPostRequestAuth('../webservices/import.php', $aPostData);
-
-		echo "<div><h3>$sTitle</h3><pre>$sCsvData</pre><div>$sRes</div></div>";
-	}
-	
-	protected function DoExecute()
-	{
-
-		$aLoads = array(
-			array(
-				'class' => 'bizOrganization',
-				'csvdata' => "name;code\nWorldCompany;WCY"
-			),
-			array(
-				'class' => 'bizLocation',
-				'csvdata' => "name;org_id;address\nParis;1;Centre de la Franca"
-			),
-			array(
-				'class' => 'bizPerson',
-				'csvdata' => "email;name;first_name;org_id;phone\njohn.foo@starac.com;Foo;John;1;+33(1)23456789"
-			),
-			array(
-				'class' => 'bizTeam',
-				'csvdata' => "name;org_id;location_name\nSquadra Azzura2;1;Paris"
-			),
-			array(
-				'class' => 'bizWorkgroup',
-				'csvdata' => "name;org_id;team_id\ntravailleurs alpins;1;6"
-			),
-			array(
-				'class' => 'bizIncidentTicket',
-				'csvdata' => "name;title;type;org_id;initial_situation;start_date;next_update;caller_id;workgroup_id;agent_id\nOVSD-12345;server down;Network;1;server was found down;2009-04-10 12:00;2009-04-10 15:00;3;317;5"
-			),
-		);  
-
-		foreach ($aLoads as $aLoadSpec)
-		{
-			$this->DoExecSingleLoad($aLoadSpec);
-		}
-	}
-}
-
-
-$aWebServices = array(
-	array(
-		'verb' => 'GetVersion',
-		'expected result' => '0.8',
-		'explain result' => 'n/a',
-		'args' => array(),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => true,
-		'explain result' => 'link attribute unknown + a CI not found',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'Server', /* sType */
-			'desc of ticket', /* sDescription */
-			'initial situation blah blah blah', /* sInitialSituation */
-			'very grave', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-				new SOAPLinkCreationSpec(
-					'logInfra',
-					array(new SOAPSearchCondition('id', 108)),
-					array(new SOAPAttributeValue('impacting', 'very critical'))
-				),
-				new SOAPLinkCreationSpec(
-					'bizDevice',
-					array(new SOAPSearchCondition('name', 'Router03')),
-					array(new SOAPAttributeValue('impact', 'who cares'))
-				),
-				new SOAPLinkCreationSpec(
-					'bizDevice',
-					array(new SOAPSearchCondition('name', 'thisone')),
-					array(new SOAPAttributeValue('impact', 'our lives'))
-				),
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'caller not specified',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'Desktop', /* sType */
-			'PC burning', /* sDescription */
-			'The power supply suddenly started to warm up', /* sInitialSituation */
-			'The agent could not do his job', /* sImpact */
-			null, /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'wrong condition on CI to attach',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'Desktop', /* sType */
-			'PC burning', /* sDescription */
-			'The power supply suddenly started to warm up', /* sInitialSituation */
-			'The agent could not do his job', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-				new SOAPLinkCreationSpec(
-					'logInfra',
-					array(new SOAPSearchCondition('dummyfiltercode', 2)),
-					array(new SOAPAttributeValue('impact', 'very critical'))
-				),
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => true,
-		'explain result' => 'no CI to attach (empty array)',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'Network', /* sType */
-			'Houston not reachable', /* sDescription */
-			'Tried to join the shuttle', /* sInitialSituation */
-			'Could not talk to my wife', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => true,
-		'explain result' => 'no CI to attach (null)',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'Network', /* sType */
-			'Houston not reachable', /* sDescription */
-			'Tried to join the shuttle', /* sInitialSituation */
-			'Could not talk to my wife', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			null, /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'caller unknown',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'Network', /* sType */
-			'Houston not reachable', /* sDescription */
-			'Tried to join the shuttle', /* sInitialSituation */
-			'Could not talk to my wife', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1000))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'wrong values for type and severity',
-		'args' => array(
-			'admin', /* sLogin */
-			'admin', /* sPassword */
-			'my type', /* sType */
-			'Houston not reachable', /* sDescription */
-			'Tried to join the shuttle', /* sInitialSituation */
-			'Could not talk to my wife', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-			), /* aImpact */
-			'my severity' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'wrong password',
-		'args' => array(
-			'admin', /* sLogin */
-			'xxxxx', /* sPassword */
-			'Network', /* sType */
-			'Houston not reachable', /* sDescription */
-			'Tried to join the shuttle', /* sInitialSituation */
-			'Could not talk to my wife', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-	array(
-		'verb' => 'CreateIncidentTicket',
-		'expected result' => false,
-		'explain result' => 'wrong login',
-		'args' => array(
-			'xxxxx', /* sLogin */
-			'yyyyy', /* sPassword */
-			'Network', /* sType */
-			'Houston not reachable', /* sDescription */
-			'Tried to join the shuttle', /* sInitialSituation */
-			'Could not talk to my wife', /* sImpact */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
-			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
-			array(
-			), /* aImpact */
-			'low' /* sSeverity */
-		),
-	),
-);
-
-
-class TestSoap extends TestSoapWebService
-{
-	static public function GetName() {return 'Test SOAP';}
-	static public function GetDescription() {return 'Do basic stuff to test the SOAP capability';}
-
-	protected function DoExecute()
-	{
-		echo "<p>Note: You may also want to try the sample SOAP client <a href=\"../webservices/itopsoap.examples.php\">itopsoap.examples.php</a></p>\n";
-
-		global $aSOAPMapping;
-
-		// this file is generated dynamically with location = here
-		$sWsdlUri = 'http'.(empty($_SERVER['HTTPS']) ? '' : 's').'://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['SCRIPT_NAME']).'/../webservices/itop.wsdl.php';
-
-		ini_set("soap.wsdl_cache_enabled","0");
-		$this->m_SoapClient = new SoapClient
-		(
-			$sWsdlUri,
-			array(
-				'classmap' => $aSOAPMapping,
-				'trace' => 1,
-			)
-		);
-
-		if (false)
-		{
-			print "<pre>\n"; 
-			print_r($this->m_SoapClient->__getTypes());
-			print "</pre>\n";
-		} 
-
-		global $aWebServices;
-		foreach ($aWebServices as $iPos => $aWebService)
-		{
-			echo "<h4>SOAP call #$iPos ".$aWebService['explain result']."</h4>\n";
-
-			try
-			{
-				$oRes = call_user_func_array(array($this->m_SoapClient, $aWebService['verb']), $aWebService['args']);
-			}
-			catch(SoapFault $e)
-			{
-				print "<pre>\n"; 
-				print "Request: \n".htmlspecialchars($this->m_SoapClient->__getLastRequest()) ."\n"; 
-				print "Response: \n".htmlspecialchars($this->m_SoapClient->__getLastResponse())."\n"; 
-				print "</pre>";
-				print "Response in HTML: <p>".$this->m_SoapClient->__getLastResponse()."</p>"; 
-				throw $e;
-			}
-
-			echo "<pre>\n";
-			print_r($oRes);
-			echo "</pre>\n";
-	
-			print "<pre>\n"; 
-			print "Request: \n".htmlspecialchars($this->m_SoapClient->__getLastRequest()) ."\n"; 
-			print "Response: \n".htmlspecialchars($this->m_SoapClient->__getLastResponse())."\n"; 
-			print "</pre>";
-
-			if ($oRes instanceof SOAPResult)
-			{
-				$res = $oRes->status;
-			}
-			else
-			{
-				$res = $oRes;
-			}
-			if ($res != $aWebService['expected result'])
-			{
-				throw new UnitTestException("Expecting result '{$aWebService['expected result']}', but got '$res'");
-			}
-		} 
-	}
-}
-
-class TestWebServicesDirect extends TestBizModel
-{
-	static public function GetName() {return 'Test web services locally';}
-	static public function GetDescription() {return 'Invoke the service directly (troubleshooting)';}
-
-	static public function GetConfigFile() {return '../config-itop.php';}
-
-	protected function DoExecute()
-	{
-		$oWebServices = new WebServices();
-
-		global $aWebServices;
-		foreach ($aWebServices as $aWebService)
-		{
-			$oRes = call_user_func_array(array($oWebServices, $aWebService['verb']), $aWebService['args']);
-			echo "<pre>\n";
-			print_r($oRes);
-			echo "</pre>\n";
-		}
-		return true;
-	}
-}
-
-class TestTriggerAndEmail extends TestBizModel
-{
-	static public function GetName() {return 'Test trigger and email';}
-	static public function GetDescription() {return 'Create a trigger and an email, then activates the trigger';}
-
-	static public function GetConfigFile() {return '../config-itop.php';}
-
-	protected function CreateEmailSpec($oTrigger, $sStatus, $sTo, $sCC, $sTesterEmail)
-	{
-		$oAction = MetaModel::NewObject("ActionEmail");
-		$oAction->Set("status", $sStatus);
-		$oAction->Set("name", "New server");
-		$oAction->Set("test_recipient", $sTesterEmail);
-		$oAction->Set("from", $sTesterEmail);
-		$oAction->Set("reply_to", $sTesterEmail);
-		$oAction->Set("to", $sTo);
-		$oAction->Set("cc", $sCC);
-		$oAction->Set("bcc", "");
-		$oAction->Set("subject", "New server: '\$this->name()$'");
-		$oAction->Set("body", "<html><body><p>Dear customer,</p><p>We have created the server \$this->hyperlink()$ in the IT infrastructure database.</p><p>You will be further notified when it is in <strong>Production</strong>.</p><p>The IT infrastructure management team.</p><p>Here are some accentuated characters for french people: 'ééà'</p></body></html>");
-		$oAction->Set("importance", "low");
-		$iActionId = $this->ObjectToDB($oAction, true);
-
-		$oLink = MetaModel::NewObject("lnkTriggerAction");
-		$oLink->Set("trigger_id", $oTrigger->GetKey());
-		$oLink->Set("action_id", $iActionId);
-		$oLink->Set("order", "1");
-		$iLink = $this->ObjectToDB($oLink, true);
-	}
-
-	protected function DoExecute()
-	{
-		$oMyPerson = MetaModel::NewObject("bizPerson");
-		$oMyPerson->Set("name", "testemail1");
-		$oMyPerson->Set("org_id", "1");
-		$oMyPerson->Set("email", "romain.quetiez@hp.com");
-		$iPersonId = $this->ObjectToDB($oMyPerson, true);
-
-		$oMyPerson = MetaModel::NewObject("bizPerson");
-		$oMyPerson->Set("name", "testemail2");
-		$oMyPerson->Set("org_id", "1");
-		$oMyPerson->Set("email", "denis.flaven@hp.com");
-		$iPersonId = $this->ObjectToDB($oMyPerson, true);
-
-		$oMyPerson = MetaModel::NewObject("bizPerson");
-		$oMyPerson->Set("name", "testemail3");
-		$oMyPerson->Set("org_id", "1");
-		$oMyPerson->Set("email", "erwan.taloc@hp.com");
-		$iPersonId = $this->ObjectToDB($oMyPerson, true);
-
-		$oMyServer = MetaModel::NewObject("bizServer");
-		$oMyServer->Set("name", "wfr.terminator.com");
-		$oMyServer->Set("severity", "low");
-		$oMyServer->Set("status", "production");
-		$oMyServer->Set("org_id", 2);
-		$oMyServer->Set("location_id", 2);
-		$iServerId = $this->ObjectToDB($oMyServer, true);
-
-		$oMyTrigger = MetaModel::NewObject("TriggerOnStateEnter");
-		$oMyTrigger->Set("description", "Testor");
-		$oMyTrigger->Set("target_class", "bizServer");
-		$oMyTrigger->Set("state", "Shipped");
-		$iTriggerId = $this->ObjectToDB($oMyTrigger, true);
-
-		// Error in OQL field(s)
-		//
-		$this->CreateEmailSpec
-		(
-			$oMyTrigger,
-			'test',
-			"SELECT bizPerson WHERE naime = 'Dali'",
-			"SELECT bizServer",
-			'romain.quetiez@hp.com'
-		);
-
-		// Error: no recipient
-		//
-		$this->CreateEmailSpec
-		(
-			$oMyTrigger,
-			'test',
-			"",
-			"",
-			'romain.quetiez@hp.com'
-		);
-
-		// Test
-		//
-		$this->CreateEmailSpec
-		(
-			$oMyTrigger,
-			'test',
-			"SELECT bizPerson WHERE name LIKE 'testemail%'",
-			"SELECT bizPerson",
-			'romain.quetiez@hp.com'
-		);
-
-		// Test failing because of a wrong test recipient address
-		//
-		$this->CreateEmailSpec
-		(
-			$oMyTrigger,
-			'test',
-			"SELECT bizPerson WHERE name LIKE 'testemail%'",
-			"",
-			'toto@walibi.bg'
-		);
-
-		// Normal behavior
-		//
-		$this->CreateEmailSpec
-		(
-			$oMyTrigger,
-			'enabled',
-			"SELECT bizPerson WHERE name LIKE 'testemail%'",
-			"",
-			'romain.quetiez@hp.com'
-		);
-
-		// Does nothing, because it is disabled
-		//
-		$this->CreateEmailSpec
-		(
-			$oMyTrigger,
-			'disabled',
-			"SELECT bizPerson WHERE name = 'testemail%'",
-			"",
-			'romain.quetiez@hp.com'
-		);
-
-		$oMyTrigger->DoActivate($oMyServer->ToArgs('this'));
-
-		return true;
-	}
-}
-?>
+<?php
+
+class TestSQLQuery extends TestScenarioOnDB
+{
+	static public function GetName() {return 'SQLQuery';}
+	static public function GetDescription() {return 'SQLQuery does not depend on the rest of the framework, therefore it makes sense to have a separate test framework for it';}
+
+	static public function GetDBHost() {return 'localhost';}
+	static public function GetDBUser() {return 'RomainDBLogin';}
+	static public function GetDBPwd() {return '';}
+	static public function GetDBName() {return 'TestSQLQuery';}
+	static public function GetDBSubName() {return 'taratata';}
+
+
+	protected function DoPrepare()
+	{
+		parent::DoPrepare();
+		cmdbSource::CreateTable('CREATE TABLE `myTable` (myKey INT(11) NOT NULL auto_increment, column1 VARCHAR(255), column2 VARCHAR(255), PRIMARY KEY (`myKey`)) ENGINE = innodb');
+		cmdbSource::CreateTable('CREATE TABLE `myTable1` (myKey1 INT(11) NOT NULL auto_increment, column1_1 VARCHAR(255), column1_2 VARCHAR(255), PRIMARY KEY (`myKey1`)) ENGINE = innodb');
+		cmdbSource::CreateTable('CREATE TABLE `myTable2` (myKey2 INT(11) NOT NULL auto_increment, column2_1 VARCHAR(255), column2_2 VARCHAR(255), PRIMARY KEY (`myKey2`)) ENGINE = innodb');
+	}
+
+	protected function DoExecute()
+	{
+		$oQuery = new SQLQuery(
+			$sTable = 'myTable',
+			$sTableAlias = 'myTableAlias',
+			$aFields = array('column1'=>new FieldExpression('column1', 'myTableAlias'), 'column2'=>new FieldExpression('column2', 'myTableAlias')),
+			$oCondition = new BinaryExpression(new FieldExpression('column1', 'myTableAlias'), 'LIKE', new ScalarExpression('trash')),
+			$aFullTextNeedles = array('column1'),
+			$bToDelete = false,
+			$aValues = array()
+		);
+		$oQuery->AddCondition(Expression::FromOQL('DATE(NOW() - 1200 * 2) > \'2008-07-31\''));
+
+		$oSubQuery1 = new SQLQuery(
+			$sTable = 'myTable1',
+			$sTableAlias = 'myTable1Alias',
+			$aFields = array('column1_1'=>new FieldExpression('column1', 'myTableAlias'), 'column1_2'=>new FieldExpression('column1', 'myTableAlias')),
+			$oCondition = new TrueSQLExpression,
+			$aFullTextNeedles = array(),
+			$bToDelete = false,
+			$aValues = array()
+		);
+
+		$oSubQuery2 = new SQLQuery(
+			$sTable = 'myTable2',
+			$sTableAlias = 'myTable2Alias',
+			$aFields = array('column2_1'=>new FieldExpression('column2', 'myTableAlias'), 'column2_2'=>new FieldExpression('column2', 'myTableAlias')),
+			$oCondition = new TrueSQLExpression,
+			$aFullTextNeedles = array(),
+			$bToDelete = false,
+			$aValues = array()
+		);
+
+		$oQuery->AddInnerJoin($oSubQuery1, 'column1', 'column1_1');
+		$oQuery->AddLeftJoin($oSubQuery2, 'column2', 'column2_2');
+		
+		$oQuery->DisplayHtml();
+		$oQuery->RenderDelete();
+		$oQuery->RenderUpdate();
+		echo '<p>'.$oQuery->RenderSelect().'</p>';
+		$oQuery->RenderSelect(array('column1'));
+		$oQuery->RenderSelect(array('column1', 'column2'));
+	}
+}
+
+class TestOQLParser extends TestFunction
+{
+	static public function GetName() {return 'Check OQL parsing';}
+	static public function GetDescription() {return 'Attempts a series of queries, and in particular those with a bad syntax';}
+
+	protected function CheckQuery($sQuery, $bIsCorrectQuery)
+	{
+		$oOql = new OqlInterpreter($sQuery);
+		try
+		{
+			$oTrash = $oOql->Parse(); // Not expecting a given format, otherwise use ParseExpression/ParseObjectQuery/ParseValueSetQuery
+			MyHelpers::var_dump_html($oTrash, true);
+		}
+		catch (OQLException $OqlException)
+		{
+			if ($bIsCorrectQuery)
+			{
+				echo "<p>More info on this unexpected failure:<br/>".$OqlException->getHtmlDesc()."</p>\n";
+				throw $OqlException;
+				return false;
+			}
+			else
+			{
+				// Everything is fine :-)
+				echo "<p>More info on this expected failure:<br/>".$OqlException->getHtmlDesc()."</p>\n";
+				return true;
+			}
+		}
+		// The query was correctly parsed, was it expected to be correct ?
+		if ($bIsCorrectQuery)
+		{
+			return true;
+		}
+		else
+		{
+			throw new UnitTestException("The query '$sQuery' was parsed with success, while it shouldn't (?)");
+			return false;
+		}
+	}
+
+	protected function TestQuery($sQuery, $bIsCorrectQuery)
+	{
+		if (!$this->CheckQuery($sQuery, $bIsCorrectQuery))
+		{
+			return false;
+		}
+		return true;
+	}
+
+	public function DoExecute()
+	{
+		$aQueries = array(
+			'SELECT toto' => true,
+			'SELECT toto WHERE toto.a = 1' => true,
+			'SELECT toto WHERE toto.a=1' => true,
+			'SELECT toto WHERE toto.a = "1"' => true,
+			'SELECT toto WHHHERE toto.a = "1"' => false,
+			'SELECT toto WHERE toto.a == "1"' => false,
+			'SELECT toto WHERE toto.a % 1' => false,
+			//'SELECT toto WHERE toto.a LIKE 1' => false,
+			'SELECT toto WHERE toto.a like \'arg\'' => false,
+			'SELECT toto WHERE toto.a NOT LIKE "That\'s it"' => true,
+			'SELECT toto WHERE toto.a NOT LIKE "That\'s "it""' => false,
+			'SELECT toto WHERE toto.a NOT LIKE "That\'s \\"it\\""' => true,
+			'SELECT toto WHERE toto.a NOT LIKE \'That"s it\'' => true,
+			'SELECT toto WHERE toto.a NOT LIKE \'That\'s it\'' => false,
+			'SELECT toto WHERE toto.a NOT LIKE \'That\\\'s it\'' => true,
+			'SELECT toto WHERE toto.a NOT LIKE "blah \\ truc"' => false,
+			'SELECT toto WHERE toto.a NOT LIKE "blah \\\\ truc"' => true,
+			'SELECT toto WHERE toto.a NOT LIKE \'blah \\ truc\'' => false,
+			'SELECT toto WHERE toto.a NOT LIKE \'blah \\\\ truc\'' => true,
+
+			'SELECT toto WHERE toto.a NOT LIKE "\\\\"' => true,
+			'SELECT toto WHERE toto.a NOT LIKE "\\""' => true,
+			'SELECT toto WHERE toto.a NOT LIKE "\\"\\\\"' => true,
+			'SELECT toto WHERE toto.a NOT LIKE "\\\\\\""' => true,
+			'SELECT toto WHERE toto.a NOT LIKE ""' => true,
+			'SELECT toto WHERE toto.a NOT LIKE "\\\\"' => true,
+			"SELECT UserRightsMatrixClassGrant WHERE UserRightsMatrixClassGrant.class = 'lnkContactRealObject' AND UserRightsMatrixClassGrant.action = 'modify' AND UserRightsMatrixClassGrant.login = 'Denis'" => true,
+			"SELECT A WHERE A.col1 = 'lit1' AND A.col2 = 'lit2' AND A.col3 = 'lit3'" => true,
+
+			'SELECT toto WHERE toto.a NOT LIKE "blah" AND toto.b LIKE "foo"' => true,
+
+			//'SELECT toto WHERE toto.a > \'asd\'' => false,
+			'SELECT toto WHERE toto.a = 1 AND toto.b LIKE "x" AND toto.f >= 12345' => true,
+			'SELECT Device JOIN Site ON Device.site = Site.id' => true,
+			'SELECT Device JOIN Site ON Device.site = Site.id JOIN Country ON Site.location = Country.id' => true,
+
+			"SELECT A JOIN B ON A.myB = B.id WHERE (A.col1 = 123 AND B.col1 = 'aa') OR (A.col3 = 'zzz' AND B.col4 > 100)" => true,
+			"SELECT A JOIN B ON A.myB = B.id WHERE (A.col1 = B.col2 AND B.col1 = A.col2) OR (A.col3 = '' AND B.col4 > 100)" => true,
+			"SELECT A JOIN B ON A.myB = B.id WHERE A.col1 + B.col2 * B.col1 = A.col2" => true,
+			"SELECT A JOIN B ON A.myB = B.id WHERE A.col1 + (B.col2 * B.col1) = A.col2" => true,
+			"SELECT A JOIN B ON A.myB = B.id WHERE (A.col1 + B.col2) * B.col1 = A.col2" => true,
+
+			'SELECT Device AS D_ JOIN Site AS S_ ON D_.site = S_.id WHERE S_.country = "Francia"' => true,
+		);
+
+		$iErrors = 0;
+
+		foreach($aQueries as $sQuery => $bIsCorrectQuery)
+		{
+			$sIsOk = $bIsCorrectQuery ? 'good' : 'bad';
+			echo "<h4>Testing query: $sQuery ($sIsOk)</h4>\n";
+			$bRet = $this->TestQuery($sQuery, $bIsCorrectQuery);
+			if (!$bRet) $iErrors++;
+		}
+		
+		return ($iErrors == 0);
+	}
+}
+
+
+class TestCSVParser extends TestFunction
+{
+	static public function GetName() {return 'Check CSV parsing';}
+	static public function GetDescription() {return 'Loads a set of CSV data';}
+
+	public function DoExecute()
+	{
+		$sDataFile = '"field1","field2","field3"
+"a","b","c"
+a,b,c
+"","",""
+,,
+"a""","b","c"
+"a1
+a2","b","c"
+"a1,a2","b","c"
+"a","b","c1,"",c2
+,c3"
+"a","b","ouf !"
+';
+
+		$sDataFile = '?field1?;?field2?;?field3?
+?a?;?b?;?c?
+a;b;c
+??;??;??
+;;
+?a"?;?b?;?c?
+?a1
+a2?;?b?;?c?
+?a1,a2?;?b?;?c?
+?a?;?b?;?c1,",c2
+,c3?
+?a?;?b?;?ouf !?
+';
+
+		echo "<pre style=\"font-style:italic;\">\n";
+		print_r($sDataFile);
+		echo "</pre>\n";
+
+		$aExpectedResult = array(
+			//array('field1', 'field2', 'field3'),
+			array('a', 'b', 'c'),
+			array('a', 'b', 'c'),
+			array('', '', ''),
+			array('', '', ''),
+			array('a"', 'b', 'c'),
+			array("a1\na2", 'b', 'c'),
+			array('a1,a2', 'b', 'c'),
+			array('a', 'b', "c1,\",c2\n,c3"),
+			array('a', 'b', 'ouf !'),
+			array('a', 'b', 'a'),
+		);
+	
+		$oCSVParser = new CSVParser($sDataFile, ';', '?');
+		$aData = $oCSVParser->ToArray(1, null, 0);
+
+		$iIssues = 0;
+
+		echo "<table border=\"1\">\n";
+		foreach ($aData as $iRow => $aRow)
+		{
+			echo "<tr>\n";
+			foreach ($aRow as $iCol => $sCell)
+			{
+				if (empty($sCell))
+				{
+					$sCellValue = '&nbsp;';
+				}
+				else
+				{
+					$sCellValue = htmlentities($sCell);
+				}
+
+				if (!isset($aExpectedResult[$iRow][$iCol]))
+				{
+					$iIssues++;
+					$sCellValue = "<span style =\"color: red; background-color: grey;\">$sCellValue</span>";
+				}
+				elseif ($aExpectedResult[$iRow][$iCol] != $sCell)
+				{
+					$iIssues++;
+					$sCellValue = "<span style =\"color: red; background-color: lightgrey;\">$sCellValue</span>, expecting '<span style =\"color: green; background-color: lightgrey;\">".$aExpectedResult[$iRow][$iCol]."</span>'";
+				}
+
+				echo "<td><pre>$sCellValue</pre></td>";
+			}
+			echo "</tr>\n";
+		}
+		echo "</table>\n";
+		return ($iIssues > 0);
+	}
+}
+
+class TestGenericItoMyModel extends TestBizModelGeneric
+{
+	static public function GetName()
+	{
+		return 'Generic RO test on '.self::GetConfigFile();
+	}
+
+	static public function GetConfigFile() {return '../config-test-mymodel.php';}
+}
+
+class TestGenericItopBigModel extends TestBizModelGeneric
+{
+	static public function GetName()
+	{
+		return 'Generic RO test on '.self::GetConfigFile();
+	}
+
+	static public function GetConfigFile() {return '../config-test-itopv06.php';}
+}
+
+class TestUserRightsMatrixItop extends TestUserRights
+{
+	static public function GetName()
+	{
+		return 'User rights test on user rights matrix';
+	}
+
+	static public function GetDescription()
+	{
+		return 'blah blah blah';
+	}
+
+	public function DoPrepare()
+	{
+		parent::DoPrepare();
+		MetaModel::Startup('../config-test-itopv06.php');
+	}
+
+	protected function DoExecute()
+	{
+		$sUser = 'Romain';
+		echo "<p>Totor: ".(UserRights::Login('Totor', 'toto') ? 'ok' : 'NO')."</p>\n";
+		echo "<p>Romain: ".(UserRights::Login('Romain', 'toto') ? 'ok' : 'NO')."</p>\n";
+		echo "<p>User: ".UserRights::GetUser()."</p>\n";
+		echo "<p>On behalf of...".UserRights::GetRealUser()."</p>\n";
+
+		echo "<p>Denis (impersonate) : ".(UserRights::Impersonate('Denis', 'tutu') ? 'ok' : 'NO')."</p>\n";
+		echo "<p>User: ".UserRights::GetUser()."</p>\n";
+		echo "<p>On behalf of...".UserRights::GetRealUser()."</p>\n";
+
+		UserRights::GetFilter('bizOrganization'); // returns a filter object
+
+		$oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT bizOrganization"));
+		echo "<p>IsActionAllowed...".(UserRights::IsActionAllowed('bizOrganization', UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES ? 'ok' : 'NO')."</p>\n";
+		echo "<p>IsStimulusAllowed...".(UserRights::IsStimulusAllowed('bizOrganization', 'myStimulus', $oSet) == UR_ALLOWED_YES ? 'ok' : 'NO')."</p>\n";
+		echo "<p>IsActionAllowedOnAttribute...".(UserRights::IsActionAllowedOnAttribute('bizOrganization', 'myattribute', UR_ACTION_MODIFY, $oSet) == UR_ALLOWED_YES ? 'ok' : 'NO')."</p>\n";
+		return true;
+	}
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Test a complex biz model on the fly
+///////////////////////////////////////////////////////////////////////////
+
+class TestMyBizModel extends TestBizModel
+{
+	static public function GetName()
+	{
+		return 'A series of tests on a weird business model';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Attempts various operations and build complex queries';
+	}
+	
+	static public function GetConfigFile() {return '../config-test-mymodel.php';}
+
+	function test_linksinfo()
+	{
+		echo "<h4>Enum links</h4>";
+		MyHelpers::var_dump_html(MetaModel::EnumReferencedClasses("cmdbTeam"));
+		MyHelpers::var_dump_html(MetaModel::EnumReferencingClasses("Organization"));
+	
+		MyHelpers::var_dump_html(MetaModel::EnumLinkingClasses());
+		MyHelpers::var_dump_html(MetaModel::EnumLinkingClasses("cmdbContact"));
+		MyHelpers::var_dump_html(MetaModel::EnumLinkingClasses("cmdWorkshop"));
+		MyHelpers::var_dump_html(MetaModel::GetLinkLabel("Liens_entre_contacts_et_workshop", "toworkshop"));
+	}
+	
+	function test_list_attributes()
+	{
+		echo "<h4>List attributes</h4>";
+		foreach(MetaModel::ListAttributeDefs("cmdbTeam") as $sAttCode=>$oAttDef)
+		{
+			echo $oAttDef->GetLabel()." / ".$oAttDef->GetDescription()." / ".$oAttDef->GetType()."</br>\n";
+		}
+	}
+	
+	function test_search()
+	{
+		echo "<h4>Two searches</h4>";
+		$oFilterAllDevs = new DBObjectSearch("cmdbTeam");
+		$oAllDevs = new DBObjectSet($oFilterAllDevs);
+		
+		echo "Found ".$oAllDevs->Count()." items.</br>\n";
+		while ($oDev = $oAllDevs->Fetch())
+		{
+			$aValues = array();
+			foreach(MetaModel::GetAttributesList($oAllDevs->GetClass()) as $sAttCode)
+			{
+				$aValues[] = MetaModel::GetLabel(get_class($oDev), $sAttCode)." (".MetaModel::GetDescription(get_class($oDev), $sAttCode).") = ".$oDev->GetAsHTML($sAttCode);
+			}
+			echo $oDev->GetKey()." => ".implode(", ", $aValues)."</br>\n";
+		}
+	
+		// a second one
+		$oMyFilter = new DBObjectSearch("cmdbContact");
+		//$oMyFilter->AddCondition("name", "aii", "Finishes with");
+		$oMyFilter->AddCondition("name", "aii");
+		$this->search_and_show_list($oMyFilter);
+		
+	}
+	
+	function test_reload()
+	{
+		echo "<h4>Reload</h4>";
+		$team = MetaModel::GetObject("cmdbContact", "2");
+		echo "Chargement de l'attribut headcount: {$team->Get("headcount")}</br>\n";
+		MyHelpers::var_dump_html($team);
+	}
+	
+	function test_setattribute()
+	{
+		echo "<h4>Set attribute and update</h4>";
+		$team = MetaModel::GetObject("cmdbTeam", "2");
+		$team->Set("headcount", rand(1,1000));
+		$team->Set("email", "Luis ".rand(9,250));
+		MyHelpers::var_dump_html($team->ListChanges());
+		echo "New headcount = {$team->Get("headcount")}</br>\n";
+		echo "Computed name = {$team->Get("name")}</br>\n";
+	
+		$oMyChange = MetaModel::NewObject("CMDBChange");
+		$oMyChange->Set("date", time());
+		$oMyChange->Set("userinfo", "test_setattribute / Made by robot #".rand(1,100));
+		$iChangeId = $oMyChange->DBInsert();
+	
+		//MetaModel::StartDebugQuery();
+		$team->DBUpdateTracked($oMyChange);
+		//MetaModel::StopDebugQuery();
+	
+		echo "<h4>Check the modified team</h4>";
+		$oTeam = MetaModel::GetObject("cmdbTeam", "2");
+		MyHelpers::var_dump_html($oTeam);
+	}
+	function test_newobject()
+	{
+		$oMyChange = MetaModel::NewObject("CMDBChange");
+		$oMyChange->Set("date", time());
+		$oMyChange->Set("userinfo", "test_newobject / Made by robot #".rand(1,100));
+		$iChangeId = $oMyChange->DBInsert();
+	
+		echo "<h4>Create a new object (team)</h4>";
+		$oNewTeam = MetaModel::NewObject("cmdbTeam");
+		$oNewTeam->Set("name", "ekip2choc #".rand(1000, 2000));
+		$oNewTeam->Set("email", "machin".rand(1,100)."@tnut.com");
+		$oNewTeam->Set("email", null);
+		$oNewTeam->Set("owner", "ITOP");
+		$oNewTeam->Set("headcount", "0".rand(38000, 38999)); // should be reset to an int value
+		$iId = $oNewTeam->DBInsertTracked($oMyChange);
+		echo "Created new team: $iId</br>";
+		echo "<h4>Delete team #$iId</h4>";
+		$oTeam = MetaModel::GetObject("cmdbTeam", $iId);
+		$oTeam->DBDeleteTracked($oMyChange);
+		echo "Deleted team: $iId</br>";
+		MyHelpers::var_dump_html($oTeam);
+	}
+	
+	
+	function test_updatecolumn()
+	{
+		$oMyChange = MetaModel::NewObject("CMDBChange");
+		$oMyChange->Set("date", time());
+		$oMyChange->Set("userinfo", "test_updatecolumn / Made by robot #".rand(1,100));
+		$iChangeId = $oMyChange->DBInsert();
+	
+		$sNewEmail = "updatecol".rand(9,250)."@quedlaballe.com";
+		echo "<h4>Update a the email: set to '$sNewEmail'</h4>";
+		$oMyFilter = new DBObjectSearch("cmdbContact");
+		$oMyFilter->AddCondition("name", "o", "Contains");
+	
+		echo "Candidates before:</br>";
+		$this->search_and_show_list($oMyFilter);
+	
+		MetaModel::BulkUpdateTracked($oMyChange, $oMyFilter, array("email" => $sNewEmail));
+	
+		echo "Candidates after:</br>";
+		$this->search_and_show_list($oMyFilter);
+	}
+	
+	function test_error()
+	{
+		trigger_error("Stop requested", E_USER_ERROR);
+	}
+	
+	function test_changetracking()
+	{
+		echo "<h4>Create a change</h4>";
+		$oMyChange = MetaModel::NewObject("CMDBChange");
+		$oMyChange->Set("date", time());
+		$oMyChange->Set("userinfo", "Made by robot #".rand(1,100));
+		$iChangeId = $oMyChange->DBInsert();
+		echo "Created new change: $iChangeId</br>";
+		MyHelpers::var_dump_html($oMyChange);
+	
+		echo "<h4>Create a new object (team)</h4>";
+		$oNewTeam = MetaModel::NewObject("cmdbTeam");
+		$oNewTeam->Set("name", "ekip2choc #".rand(1000, 2000));
+		$oNewTeam->Set("email", "machin".rand(1,100)."@tnut.com");
+		$oNewTeam->Set("email", null);
+		$oNewTeam->Set("owner", "ITOP");
+		$oNewTeam->Set("headcount", "0".rand(38000, 38999)); // should be reset to an int value
+		$iId = $oNewTeam->DBInsertTracked($oMyChange);
+		echo "Created new team: $iId</br>";
+		echo "<h4>Delete team #$iId</h4>";
+		$oTeam = MetaModel::GetObject("cmdbTeam", $iId);
+		$oTeam->DBDeleteTracked($oMyChange);
+		echo "Deleted team: $iId</br>";
+		MyHelpers::var_dump_html($oTeam);
+	}
+	
+	function test_zlist()
+	{
+		echo "<h4>Test ZLists</h4>";
+		$aZLists = MetaModel::EnumZLists();
+		foreach ($aZLists as $sListCode)
+		{
+			$aListInfos = MetaModel::GetZListInfo($sListCode);
+			echo "<h4>List '".$sListCode."' (".$aListInfos["description"].") of type '".$aListInfos["type"]."'</h5>\n";
+	
+			foreach (MetaModel::GetSubclasses("cmdbObjectHomeMade") as $sKlass)
+			{
+				$aItems = MetaModel::GetZListItems($sKlass, $sListCode);
+				if (count($aItems) == 0) continue;
+	
+				echo "$sKlass - $sListCode : {".implode(", ", $aItems)."}</br>\n";
+			}
+		}
+	
+		echo "<h4>IsAttributeInZList()... </h4>";
+		echo "Liens_entre_contacts_et_workshop::ws_info in list1 ? ".(MetaModel::IsAttributeInZList("Liens_entre_contacts_et_workshop", "list1", "ws_info") ? "yes" : "no")."</br>\n";
+		echo "Liens_entre_contacts_et_workshop::toworkshop in list1 ? ".(MetaModel::IsAttributeInZList("Liens_entre_contacts_et_workshop", "list1", "toworkshop") ? "yes" : "no")."</br>\n";
+	
+	}
+	
+	function test_SibuSQL()
+	{
+		echo "<h4>Simple But Structured Query Language</h4>";
+	
+		$oMyFilter = new DBObjectSearch("cmdbContact");
+		echo "Tous les contacts: ".$oMyFilter->ToSibuSQL()."<br/>\n";
+		$oNewFilter = DBObjectSearch::FromSibuSQL($oMyFilter->ToSibuSQL());
+		echo "En passant par un filtre, ca revient en : ".$oNewFilter->ToSibuSQL()."</br>\n";
+		$this->search_and_show_list($oNewFilter);
+	
+		$sFilterDesc = "cmdbContact: name Begins with '$[debutnom:as:debut du nom]' AND ownername NotLike $[ddd::]"; 
+		echo "Construction d'un filtre a partir de sa description en SibuSQL: $sFilterDesc<br/>\n";
+	
+		MyHelpers::var_dump_html(DBObjectSearch::ListSibusQLParams($sFilterDesc));
+		$oNewFilter = DBObjectSearch::FromSibuSQL($sFilterDesc, array('ddd'=>123));
+		echo "Ca revient en: ".$oNewFilter->ToSibuSQL();
+	}
+	
+	function test_pkey()
+	{
+		echo "<h4>Test search on pkey</h4>";
+		$sExpr1 = "cmdbContact: pkey IN {40, 42}";
+		$sExpr2 = "cmdbContact: pkey NOTIN {40, 42}";
+		$this->search_and_show_list_from_sibusql($sExpr1);
+		$this->search_and_show_list_from_sibusql($sExpr2);
+	
+		echo "Et maintenant, on fusionne....</br>\n";
+		$oSet1 = new CMDBObjectSet(DBObjectSearch::FromSibuSQL($sExpr1));
+		$oSet2 = new CMDBObjectSet(DBObjectSearch::FromSibuSQL($sExpr2));
+		$oIntersect = $oSet1->CreateIntersect($oSet2);
+		$oDelta = $oSet1->CreateDelta($oSet2);
+	
+		$oMerge = clone $oSet1;
+		$oMerge->Merge($oSet2);
+		$oMerge->Merge($oSet2);
+	
+		echo "Set1 - Found ".$oSet1->Count()." items.</br>\n";
+		echo "Set2 - Found ".$oSet2->Count()." items.</br>\n";
+		echo "Intersect - Found ".$oIntersect->Count()." items.</br>\n";
+		echo "Delta - Found ".$oDelta->Count()." items.</br>\n";
+		echo "Merge - Found ".$oMerge->Count()." items.</br>\n";
+		//$this->show_list($oObjSet);
+	}
+	
+	function test_relations()
+	{
+		echo "<h4>Test relations</h4>";
+		
+		//MyHelpers::var_dump_html(MetaModel::EnumRelationQueries("cmdbObjectHomeMade", "Potes"));
+		MyHelpers::var_dump_html(MetaModel::EnumRelationQueries("cmdbContact", "Potes"));
+	
+		$iMaxDepth = 9;
+		echo "Max depth = $iMaxDepth</br>\n";
+	
+		$oObj = MetaModel::GetObject("cmdbContact", 18);
+		$aRels = $oObj->GetRelatedObjects("Potes", $iMaxDepth);
+		echo $oObj->Get('name')." has some 'Potes'...</br>\n";
+		foreach ($aRels as $sClass => $aObjs)
+		{
+			echo "$sClass, count = ".count($aObjs)." =&gt; ".implode(', ', array_keys($aObjs))."</br>\n";
+			$oObjectSet = CMDBObjectSet::FromArray($sClass, $aObjs);
+			$this->show_list($oObjectSet);
+		}
+	
+		echo "<h4>Test relations - same results, by the mean of a SibuSQL</h4>";
+		$this->search_and_show_list_from_sibusql("cmdbContact: RELATED (Potes, $iMaxDepth) TO (cmdbContact: pkey = 18)");
+		
+	}
+	
+	function test_linkedset()
+	{
+		echo "<h4>Linked set attributes</h4>\n";
+		$oObj = MetaModel::GetObject("cmdbContact", 18);
+		
+		echo "<h5>Current workshops</h5>\n";
+		$oSetWorkshopsCurr = $oObj->Get("myworkshops");
+		$this->show_list($oSetWorkshopsCurr);
+	
+		echo "<h5>Setting workshops</h5>\n";
+		$oNewLink = new cmdbLiens();
+		$oNewLink->Set('toworkshop', 2);
+		$oNewLink->Set('function', 'mafonctioooon');
+		$oNewLink->Set('a1', 'tralala1');
+		$oNewLink->Set('a2', 'F7M');
+		$oSetWorkshops = CMDBObjectSet::FromArray("cmdbLiens", array($oNewLink));
+		$oObj->Set("myworkshops", $oSetWorkshops); 
+		$this->show_list($oSetWorkshops);
+	
+		echo "<h5>New workshops</h5>\n";
+		$oSetWorkshopsCurr = $oObj->Get("myworkshops");
+		$this->show_list($oSetWorkshopsCurr);
+	
+		$oMyChange = MetaModel::NewObject("CMDBChange");
+		$oMyChange->Set("date", time());
+		$oMyChange->Set("userinfo", "test_linkedset / Made by robot #".rand(1,100));
+		$iChangeId = $oMyChange->DBInsert();
+		$oObj->DBUpdateTracked($oMyChange);
+		$oObj = MetaModel::GetObject("cmdbContact", 18);
+	
+		echo "<h5>After the write</h5>\n";
+		$oSetWorkshopsCurr = $oObj->Get("myworkshops");
+		$this->show_list($oSetWorkshopsCurr);
+	}
+	
+	function test_object_lifecycle()
+	{
+		echo "<h4>Test object lifecycle</h4>";
+	
+	
+		MyHelpers::var_dump_html(MetaModel::GetStateAttributeCode("cmdbContact"));
+		MyHelpers::var_dump_html(MetaModel::EnumStates("cmdbContact"));
+		MyHelpers::var_dump_html(MetaModel::EnumStimuli("cmdbContact"));
+		foreach(MetaModel::EnumStates("cmdbContact") as $sStateCode => $aStateDef)
+		{
+			echo "<p>Transition from <strong>$sStateCode</strong></p>\n";
+			MyHelpers::var_dump_html(MetaModel::EnumTransitions("cmdbContact", $sStateCode));
+		}
+	
+		$oObj = MetaModel::GetObject("cmdbContact", 18);
+		echo "Current state: ".$oObj->GetState()."... let's go to school...";
+		MyHelpers::var_dump_html($oObj->EnumTransitions());
+		$oObj->ApplyStimulus("toschool");
+		echo "New state: ".$oObj->GetState()."... let's get older...";
+		MyHelpers::var_dump_html($oObj->EnumTransitions());
+		$oObj->ApplyStimulus("raise");
+		echo "New state: ".$oObj->GetState()."... let's try to go further... (should give an error)";
+		MyHelpers::var_dump_html($oObj->EnumTransitions());
+		$oObj->ApplyStimulus("raise"); // should give an error
+	}
+
+
+	protected function DoExecute()
+	{
+//				$this->ReportError("Found two different SibuSQL expression out of the (same?) filter: <em>$sExpr1</em> != <em>$sExpr2</em>");
+//			$this->ReportSuccess('Found '.$oSet->Count()." objects of class $sClassName");
+		//$this->test_linksinfo();
+		//$this->test_list_attributes();
+		//$this->test_search();
+		//$this->test_reload();
+		//$this->test_newobject();
+		$this->test_setattribute();
+		//$this->test_updatecolumn();
+		//$this->test_error();
+		//$this->test_changetracking();
+		$this->test_zlist();
+		$this->test_SibuSQL();
+		//$this->test_pkey();
+		$this->test_relations();
+		$this->test_linkedset();
+		$this->test_object_lifecycle();
+	}
+}
+
+
+///////////////////////////////////////////////////////////////////////////
+// Test a complex biz model on the fly
+///////////////////////////////////////////////////////////////////////////
+
+abstract class MyFarm extends TestBizModel
+{
+	static public function GetConfigFile() {return '../config-test-farm.php';}
+
+	protected function DoPrepare()
+	{
+		parent::DoPrepare();
+		$this->ResetDB();
+		MetaModel::DBCheckIntegrity();
+	}
+
+	protected function InsertMammal($sSpecies, $sSex, $iSpeed, $iMotherid, $iFatherId, $sName, $iHeight, $sBirth)
+	{
+		$oNew = MetaModel::NewObject('Mammal');
+		$oNew->Set('species', $sSpecies);
+		$oNew->Set('sex', $sSex);
+		$oNew->Set('speed', $iSpeed);
+		$oNew->Set('mother', $iMotherid);
+		$oNew->Set('father', $iFatherId);
+		$oNew->Set('name', $sName);
+		$oNew->Set('height', $iHeight);
+		$oNew->Set('birth', $sBirth);
+		return $this->ObjectToDB($oNew);
+	}
+
+	protected function InsertBird($sSpecies, $sSex, $iSpeed, $iMotherid, $iFatherId)
+	{
+		$oNew = MetaModel::NewObject('Bird');
+		$oNew->Set('species', $sSpecies);
+		$oNew->Set('sex', $sSex);
+		$oNew->Set('speed', $iSpeed);
+		$oNew->Set('mother', $iMotherid);
+		$oNew->Set('father', $iFatherId);
+		return $this->ObjectToDB($oNew);
+	}
+
+	protected function InsertFlyingBird($sSpecies, $sSex, $iSpeed, $iMotherid, $iFatherId, $iFlyingSpeed)
+	{
+		$oNew = MetaModel::NewObject('FlyingBird');
+		$oNew->Set('species', $sSpecies);
+		$oNew->Set('sex', $sSex);
+		$oNew->Set('speed', $iSpeed);
+		$oNew->Set('mother', $iMotherid);
+		$oNew->Set('father', $iFatherId);
+		$oNew->Set('flyingspeed', $iFlyingSpeed);
+		return $this->ObjectToDB($oNew);
+	}
+
+	private function InsertGroup($sName, $iLeaderId)
+	{
+		$oNew = MetaModel::NewObject('Group');
+		$oNew->Set('name', $sName);
+		$oNew->Set('leader', $iLeaderId);
+		$iId = $oNew->DBInsertNoReload();
+		return $iId;
+	}
+}
+
+
+class TestQueriesOnFarm extends MyFarm
+{
+	static public function GetName()
+	{
+		return 'Farm test';
+	}
+
+	static public function GetDescription()
+	{
+		return 'A series of tests on the farm business model (SQL generation)';
+	}
+
+	protected function CheckQuery($sQuery, $bIsCorrectQuery)
+	{
+		if ($bIsCorrectQuery)
+		{
+			echo "<h4 style=\"color:green;\">$sQuery</h4>\n";
+		}
+		else
+		{
+			echo "<h4 style=\"color:red;\">$sQuery</h3>\n";
+		}
+		try
+		{
+			//$oOql = new OqlInterpreter($sQuery);
+			//$oTrash = $oOql->ParseObjectQuery();
+			//MyHelpers::var_dump_html($oTrash, true);
+			$oMyFilter = DBObjectSearch::FromOQL($sQuery);
+		}
+		catch (OQLException $oOqlException)
+		{
+			if ($bIsCorrectQuery)
+			{
+				echo "<p>More info on this unexpected failure:<br/>".$oOqlException->getHtmlDesc()."</p>\n";
+				throw $oOqlException;
+				return false;
+			}
+			else
+			{
+				// Everything is fine :-)
+				echo "<p>More info on this expected failure:\n";
+				echo "<ul>\n";
+				echo "<li>".get_class($oOqlException)."</li>\n";
+				echo "<li>".$oOqlException->getMessage()."</li>\n";
+				echo "<li>".$oOqlException->getHtmlDesc()."</li>\n";
+				echo "</ul>\n";
+				echo "</p>\n";
+				return true;
+			}
+		}
+		// The query was correctly parsed, was it expected to be correct ?
+		if (!$bIsCorrectQuery)
+		{
+			throw new UnitTestException("The query '$sQuery' was parsed with success, while it shouldn't (?)");
+			return false;
+		}
+		echo "<p>To OQL: ".$oMyFilter->ToOQL()."</p>";
+
+		$this->search_and_show_list($oMyFilter);
+		
+		//echo "<p>first pass<p>\n";
+		//MyHelpers::var_dump_html($oMyFilter, true);
+		$sQuery1 = MetaModel::MakeSelectQuery($oMyFilter);
+		//echo "<p>second pass<p>\n";
+		//MyHelpers::var_dump_html($oMyFilter, true);
+		//$sQuery1 = MetaModel::MakeSelectQuery($oMyFilter);
+		
+		$sSerialize = $oMyFilter->serialize();
+		echo "<p>Serialized:$sSerialize</p>\n";
+		$oFilter2 = DBObjectSearch::unserialize($sSerialize);
+		try
+		{
+			$sQuery2 = MetaModel::MakeSelectQuery($oFilter2);
+		}
+		catch (Exception $e)
+		{
+			echo "<p>Could not compute the query after unserialize</p>\n";
+			echo "<p>Query 1: $sQuery1</p>\n";
+			MyHelpers::var_cmp_html($oMyFilter, $oFilter2);
+			throw $e;
+		}
+		//if ($oFilter2 != $oMyFilter) no, they may differ while the resulting query is the same!
+		if ($sQuery1 != $sQuery2)
+		{
+			echo "<p>serialize/unserialize mismatch :-(</p>\n";
+			MyHelpers::var_cmp_html($sQuery1, $sQuery2);
+			MyHelpers::var_cmp_html($oMyFilter, $oFilter2);
+			return false;
+		}
+		return true;
+	}
+
+	protected function DoExecute()
+	{
+//			$this->ReportError("Found two different SibuSQL expression out of the (same?) filter: <em>$sExpr1</em> != <em>$sExpr2</em>");
+//			$this->ReportSuccess('Found '.$oSet->Count()." objects of class $sClassName");
+		echo "<h3>Create protagonists...</h3>";
+
+		$iId1 = $this->InsertMammal('human', 'male', 10, 0, 0, 'romanoff', 192, '1971-07-19');
+		$iId2 = $this->InsertMammal('human', 'female', 9, 0, 0, 'rouanita', 165, '1983-01-23');
+		$this->InsertMammal('human', 'female', 3, $iId2, $iId1, 'pomme', 169, '2008-02-23');
+		$this->InsertMammal('pig', 'female', 3, 0, 0, 'grouinkette', 85, '2006-06-01');
+		$this->InsertMammal('donkey', 'female', 3, 0, 0, 'muleta', 124, '2003-11-11');
+
+		$this->InsertBird('rooster', 'male', 12, 0, 0);
+		$this->InsertFlyingBird('pie', 'female', 11, 0, 0, 35);
+
+		// Benchmarking
+		//
+		if (false)
+		{
+			define ('COUNT_BENCHMARK', 10);
+			echo "<h3>Parsing a long query, ".COUNT_BENCHMARK." times</h3>";
+			$sQuery = "SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.birth < DATE_SUB(CURRENT_DATE(), INTERVAL 10 YEAR) AND Dad.height * 2 <= ROUND(TO_DAYS(Dad.birth) / (3 + 1) * 5 - 3)";
+	
+			$fStart = MyHelpers::getmicrotime();
+			for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
+			{
+				$oMyFilter = DBObjectSearch::FromOQL($sQuery);
+			}
+			$fDuration = MyHelpers::getmicrotime() - $fStart;
+			$fParsingDuration = $fDuration / COUNT_BENCHMARK;
+			echo "<p>Mean time by op: $fParsingDuration</p>";
+		}
+
+		echo "<h3>Test queries...</h3>";
+
+		$aQueries = array(
+			'SELECT Animal' => true,
+			'SELECT Animal WHERE Animal.pkey = 1' => false,
+			'SELECT Animal WHERE Animal.id = 1' => true,
+			'SELECT Aniiimal' => false,
+			'SELECTe Animal' => false,
+			'SELECT * FROM Animal' => false,
+			'SELECT Animal AS zoo WHERE zoo.species = \'human\'' => true,
+			'SELECT Animal AS zoo WHERE species = \'human\'' => true,
+			'SELECT Animal AS zoo WHERE espece = \'human\'' => false,
+			'SELECT Animal AS zoo WHERE zoo.species IN (\'human\', "pig")' => true,
+			'SELECT Animal AS zoo WHERE CONCATENATION(zoo.species, zoo.sex) LIKE "hum%male"' => false,
+			'SELECT Animal AS zoo WHERE CONCAT(zoo.species, zoo.sex) LIKE "hum%male"' => true,
+			'SELECT Animal AS zoo WHERE zoo.species NOT IN (\'human\', "pig")' => true,
+			'SELECT Animal AS zoo WHERE zoo.kind = \'human\'' => false,
+			'SELECT Animal WHERE Animal.species = \'human\' AND Animal.sex = \'female\'' => true,
+			'SELECT Mammal AS x WHERE (x.species = \'human\' AND x.name LIKE \'ro%\') OR (x.species = \'donkey\' AND x.name LIKE \'po%\')' => true,
+			'SELECT Mammal AS x WHERE x.species = \'human\' AND x.name LIKE \'ro%\' OR x.species = \'donkey\' AND x.name LIKE \'po%\'' => true,
+			'SELECT Mammal AS m WHERE MONTH(m.birth) = 7' => true,
+			'SELECT Mammal AS m WHERE DAY(m.birth) = 19' => true,
+			'SELECT Mammal AS m WHERE YEAR(m.birth) = 1971' => true,
+			'SELECT Mammal AS m WHERE m.birth < DATE_SUB(CURRENT_DATE(), INTERVAL 10 YEAR)' => true,
+			'SELECT Mammal AS m WHERE m.birth > DATE_SUB(NOW(), INTERVAL 2000 DAY)' => true,
+			'SELECT Mammal AS m WHERE (TO_DAYS(NOW()) - TO_DAYS(m.birth)) > 2000' => true,
+			'SELECT Mammal AS m WHERE m.name = IF(FLOOR(ROUND(m.height)) > 2, "pomme", "romain")' => true,
+			'SELECT Mammal AS m WHERE (1 + 2' => false,
+			'SELECT Mammal AS m WHERE (1 + 2 * 4 / 23) > 0' => true,
+			'SELECT Mammal AS m WHERE (4 / 23 * 2 + 1) > 0' => true,
+			'SELECT Mammal AS m WHERE 1/0' => true,
+			'SELECT Mammal AS m WHERE MONTH(m.birth) = 7' => true,
+			'SELECT Animal JOIN Group ON Group.leader = Animal.id' => true,
+			'SELECT Group JOIN Animal ON Group.leader = Animal.id' => true,
+			'SELECT Animal AS A JOIN Group AS G1 ON G1.leader = A.id' => true,
+			'SELECT Animal AS A JOIN Group AS G ON FooClass.leader = A.id' => false,
+			'SELECT Animal AS A JOIN Group AS G ON G.leader = FooClass.id' => false,
+			'SELECT Animal AS A JOIN Group AS G ON G.masterchief = A.id' => false,
+			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.pkey' => false,
+			'SELECT Animal AS A JOIN Group AS G ON A.id = G.leader' => false,
+			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE A.sex=\'male\' OR G.qwerty = 123' => false,
+			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE A.sex=\'male\' OR G.name LIKE "a%"' => true,
+			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE A.id = 1' => true,
+			'SELECT Animal AS A JOIN Group AS G ON G.leader = A.id WHERE id = 1' => false,
+			'SELECT Animal AS A JOIN Group AS G ON A.member = G.id' => false,
+			'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id' => true,
+			'SELECT Mammal AS M JOIN Group AS G ON A.member = G.id' => false,
+			'SELECT Mammal AS myAlias JOIN Group AS myAlias ON myAlias.member = myAlias.id' => false,
+			'SELECT Mammal AS Mammal JOIN Group AS Mammal ON Mammal.member = Mammal.id' => false,
+			'SELECT Group AS G WHERE G.leader_name LIKE "%"' => true,
+			'SELECT Group AS G WHERE G.leader_speed < 100000' => true,
+			'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id WHERE G.leader_name LIKE "%"' => true,
+			'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id WHERE G.leader_speed < 100000' => true,
+			'SELECT Mammal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id' => true,
+			'SELECT Mammal AS Child JOIN Animal AS Dad ON Child.father = Dad.id' => true,
+			'SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id' => true,
+			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id' => true,
+			'SELECT Animal AS Dad JOIN Animal AS Child ON Child.father = Dad.id' => true,
+			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id' => true,
+			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.id = 1' => true,
+			'SELECT Animal AS Child JOIN Animal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.name = \'romanoff\'' => false,
+			'SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id' => true,
+			'SELECT Animal AS Child JOIN Mammal AS Dad ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id WHERE Dad.name = \'romanoff\' OR Mum.speed = 0' => true,
+			'SELECT Animal AS Dad JOIN Animal AS Child ON Child.father = Dad.id JOIN Animal AS Mum ON Child.mother = Mum.id' => true,
+			'SELECT Mammal AS Dad JOIN Mammal AS Child ON Child.father = Dad.id' => true,
+			'SELECT Mammal AS Dad JOIN Mammal AS Child ON Child.father = Dad.id JOIN Mammal AS Mum ON Child.mother = Mum.id WHERE Dad.name = \'romanoff\' OR Mum.name=\'chloe\' OR Child.name=\'bizounours\'' => true,
+		);
+		//$aQueries = array(
+		//	'SELECT Mammal AS M JOIN Group AS G ON M.member = G.id WHERE G.leader_name LIKE "%"' => true,
+		//);
+		foreach($aQueries as $sQuery => $bIsCorrect)
+		{
+			$this->CheckQuery($sQuery, $bIsCorrect);
+		}
+		return true;
+	}
+}
+
+
+///////////////////////////////////////////////////////////////////////////
+// Test data load
+///////////////////////////////////////////////////////////////////////////
+
+class TestBulkChangeOnFarm extends TestBizModel
+{
+	static public function GetName()
+	{
+		return 'Farm test - data load';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Bulk load';
+	}
+	
+	static public function GetConfigFile() {return '../config-test-farm.php';}
+
+	protected function DoPrepare()
+	{
+		parent::DoPrepare();
+		$this->ResetDB();
+		MetaModel::DBCheckIntegrity();
+	}
+
+	protected function DoExecute()
+	{
+//			$this->ReportError("Found two different SibuSQL expression out of the (same?) filter: <em>$sExpr1</em> != <em>$sExpr2</em>");
+//			$this->ReportSuccess('Found '.$oSet->Count()." objects of class $sClassName");
+
+		$oParser = new CSVParser("denomination,hauteur,age
+		suzy,123,2009-01-01
+		chita,456,
+		");
+		$aData = $oParser->ToArray(array('_name', '_height', '_birth'), ',');
+		MyHelpers::var_dump_html($aData);
+
+		$oBulk = new BulkChange(
+			'Mammal',
+			$aData,
+			array('name' => '_name', 'height' => '_height', 'birth' => '_birth'),
+			array('name'),
+			array()
+		);
+
+		$oMyChange = MetaModel::NewObject("CMDBChange");
+		$oMyChange->Set("date", time());
+		$oMyChange->Set("userinfo", "Testor");
+		$iChangeId = $oMyChange->DBInsert();
+//		echo "Created new change: $iChangeId</br>";
+
+		echo "<h3>Planned for loading...</h3>";
+		$aRes = $oBulk->Process();
+		print_r($aRes);
+		echo "<h3>Go for loading...</h3>";
+		$aRes = $oBulk->Process($oMyChange);
+		print_r($aRes);
+
+		return;
+
+		$oRawData = array(
+			'Mammal',
+			array('species', 'sex', 'speed', 'mother', 'father', 'name', 'height', 'birth'),
+			"human,male,23,0,0,romulus,192,1971
+			human,male,23,0,0,remus,154,-50
+			human,male,23,0,0,julius,160,-49
+			human,female,23,0,0,cleopatra,142,-50
+			pig,female,23,0,0,confucius,50,2003"
+		);
+	}
+}
+
+
+///////////////////////////////////////////////////////////////////////////
+// Test data load
+///////////////////////////////////////////////////////////////////////////
+
+class TestFullTextSearchOnFarm extends MyFarm
+{
+	static public function GetName()
+	{
+		return 'Farm test - full text search';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Focus on the full text search feature';
+	}
+	
+	protected function DoExecute()
+	{
+		echo "<h3>Create protagonists...</h3>";
+
+		$iId1 = $this->InsertMammal('human', 'male', 10, 0, 0, 'romanoff', 192, '1971-07-19');
+		$iId2 = $this->InsertMammal('human', 'female', 9, 0, 0, 'rouanita', 165, '1983-01-23');
+		$this->InsertMammal('human', 'female', 3, $iId2, $iId1, 'pomme', 169, '2008-02-23');
+		$this->InsertMammal('pig', 'female', 3, 0, 0, 'grouinkette', 85, '2006-06-01');
+		$this->InsertMammal('donkey', 'female', 3, 0, 0, 'muleta', 124, '2003-11-11');
+
+		$this->InsertBird('rooster', 'male', 12, 0, 0);
+		$this->InsertFlyingBird('pie', 'female', 11, 0, 0, 35);
+
+		echo "<h3>Search...</h3>";
+		$oSearch = new DBObjectSearch('Mammal');
+		$oSearch->AddCondition_FullText('manof');
+		//$oResultSet = new DBObjectSet($oSearch);
+		$this->search_and_show_list($oSearch);
+	}
+}
+
+
+///////////////////////////////////////////////////////////////////////////
+// Benchmark queries
+///////////////////////////////////////////////////////////////////////////
+
+class TestItopEfficiency extends TestBizModel
+{
+	static public function GetName()
+	{
+		return 'Itop - benchmark';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Measure time to perform the queries';
+	}
+
+	static public function GetConfigFile() {return '../config-itop.php';}
+
+	protected function DoBenchmark($sOqlQuery)
+	{
+		echo "<h3>Testing query: $sOqlQuery</h3>";
+
+		$fStart = MyHelpers::getmicrotime();
+		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
+		{
+			$oFilter = DBObjectSearch::FromOQL($sOqlQuery);
+		}
+		$fDuration = MyHelpers::getmicrotime() - $fStart;
+		$fParsingDuration = $fDuration / COUNT_BENCHMARK;
+
+		$fStart = MyHelpers::getmicrotime();
+		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
+		{
+			$sSQL = MetaModel::MakeSelectQuery($oFilter);
+		}
+		$fDuration = MyHelpers::getmicrotime() - $fStart;
+		$fBuildDuration = $fDuration / COUNT_BENCHMARK;
+
+		$fStart = MyHelpers::getmicrotime();
+		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
+		{
+			$res = CMDBSource::Query($sSQL);
+		}
+		$fDuration = MyHelpers::getmicrotime() - $fStart;
+		$fQueryDuration = $fDuration / COUNT_BENCHMARK;
+
+		// The fetch could not be repeated with the same results
+		// But we've seen so far that is was very very quick to exec
+		// So it makes sense to benchmark it a single time
+		$fStart = MyHelpers::getmicrotime();
+		$aRow = CMDBSource::FetchArray($res);
+		$fDuration = MyHelpers::getmicrotime() - $fStart;
+		$fFetchDuration = $fDuration;
+
+		$fStart = MyHelpers::getmicrotime();
+		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
+		{
+			$sOql = $oFilter->ToOQL();
+		}
+		$fDuration = MyHelpers::getmicrotime() - $fStart;
+		$fToOqlDuration = $fDuration / COUNT_BENCHMARK;
+
+		echo "<ul>\n";
+		echo "<li>Parsing: $fParsingDuration</li>\n";
+		echo "<li>Build: $fBuildDuration</li>\n";
+		echo "<li>Query: $fQueryDuration</li>\n";
+		echo "<li>Fetch: $fFetchDuration</li>\n";
+		echo "<li>ToOql: $fToOqlDuration</li>\n";
+		echo "</ul>\n";
+
+		// Everything but the ToOQL (wich is interesting, anyhow)
+		$fTotal = $fParsingDuration + $fBuildDuration + $fQueryDuration + $fFetchDuration; 
+
+		return array(
+			'rows' => CMDBSource::NbRows($res),
+			'duration (s)' => round($fTotal, 4),
+			'parsing (%)' => round(100 * $fParsingDuration / $fTotal, 1),
+			'build SQL (%)' => round(100 * $fBuildDuration / $fTotal, 1),
+			'query exec (%)' => round(100 * $fQueryDuration / $fTotal, 1),
+			'fetch (%)' => round(100 * $fFetchDuration / $fTotal, 1),
+			'to OQL (%)' => round(100 * $fToOqlDuration / $fTotal, 1),
+			'parsing+build (%)' => round(100 * ($fParsingDuration + $fBuildDuration) / $fTotal, 1),
+		);
+	}
+	
+	protected function DoExecute()
+	{
+		define ('COUNT_BENCHMARK', 3);
+		echo "<p>The test will be repeated ".COUNT_BENCHMARK." times</p>";
+
+		$aQueries = array(
+			'SELECT CMDBChangeOpSetAttribute',
+			'SELECT CMDBChangeOpSetAttribute WHERE id=10',
+			'SELECT CMDBChangeOpSetAttribute WHERE id=123456789',
+			'SELECT CMDBChangeOpSetAttribute WHERE CMDBChangeOpSetAttribute.id=10',
+			'SELECT bizIncidentTicket',
+			'SELECT bizIncidentTicket WHERE id=1',
+			'SELECT bizPerson',
+			'SELECT bizPerson WHERE id=1',
+			'SELECT bizIncidentTicket JOIN bizPerson ON bizIncidentTicket.agent_id = bizPerson.id WHERE bizPerson.id = 5',
+		);
+		$aStats  = array();
+		foreach ($aQueries as $sOQL)
+		{
+			$aStats[$sOQL] = $this->DoBenchmark($sOQL);
+		}
+
+		$aData = array();
+		foreach ($aStats as $sOQL => $aResults)
+		{
+			$aValues = array();
+			$aValues['OQL'] = htmlentities($sOQL);
+
+			foreach($aResults as $sDesc => $sInfo)
+			{
+				$aValues[$sDesc] = htmlentities($sInfo);
+			}
+			$aData[] = $aValues;
+		}
+		echo MyHelpers::make_table_from_assoc_array($aData);
+	}
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Test data load
+///////////////////////////////////////////////////////////////////////////
+
+class TestItopWebServices extends TestWebServices
+{
+	static public function GetName()
+	{
+		return 'Itop - web services';
+	}
+
+	static public function GetDescription()
+	{
+		return 'Bulk load and ???';
+	}
+
+	protected function DoExecSingleLoad($aLoadSpec)
+	{
+		$sTitle = 'Load: '.$aLoadSpec['class'];
+		$sClass = $aLoadSpec['class'];
+		$sCsvData = $aLoadSpec['csvdata'];
+
+		$aPostData = array('class' => $sClass, 'csvdata' => $sCsvData);
+		$sRes = self::DoPostRequestAuth('../webservices/import.php', $aPostData);
+
+		echo "<div><h3>$sTitle</h3><pre>$sCsvData</pre><div>$sRes</div></div>";
+	}
+	
+	protected function DoExecute()
+	{
+
+		$aLoads = array(
+			array(
+				'class' => 'bizOrganization',
+				'csvdata' => "name;code\nWorldCompany;WCY"
+			),
+			array(
+				'class' => 'bizLocation',
+				'csvdata' => "name;org_id;address\nParis;1;Centre de la Franca"
+			),
+			array(
+				'class' => 'bizPerson',
+				'csvdata' => "email;name;first_name;org_id;phone\njohn.foo@starac.com;Foo;John;1;+33(1)23456789"
+			),
+			array(
+				'class' => 'bizTeam',
+				'csvdata' => "name;org_id;location_name\nSquadra Azzura2;1;Paris"
+			),
+			array(
+				'class' => 'bizWorkgroup',
+				'csvdata' => "name;org_id;team_id\ntravailleurs alpins;1;6"
+			),
+			array(
+				'class' => 'bizIncidentTicket',
+				'csvdata' => "name;title;type;org_id;initial_situation;start_date;next_update;caller_id;workgroup_id;agent_id\nOVSD-12345;server down;Network;1;server was found down;2009-04-10 12:00;2009-04-10 15:00;3;317;5"
+			),
+		);  
+
+		foreach ($aLoads as $aLoadSpec)
+		{
+			$this->DoExecSingleLoad($aLoadSpec);
+		}
+	}
+}
+
+
+$aWebServices = array(
+	array(
+		'verb' => 'GetVersion',
+		'expected result' => '0.8',
+		'explain result' => 'n/a',
+		'args' => array(),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => true,
+		'explain result' => 'link attribute unknown + a CI not found',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Server', /* sType */
+			'desc of ticket', /* sDescription */
+			'initial situation blah blah blah', /* sInitialSituation */
+			'very grave', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+				new SOAPLinkCreationSpec(
+					'logInfra',
+					array(new SOAPSearchCondition('id', 108)),
+					array(new SOAPAttributeValue('impacting', 'very critical'))
+				),
+				new SOAPLinkCreationSpec(
+					'bizDevice',
+					array(new SOAPSearchCondition('name', 'Router03')),
+					array(new SOAPAttributeValue('impact', 'who cares'))
+				),
+				new SOAPLinkCreationSpec(
+					'bizDevice',
+					array(new SOAPSearchCondition('name', 'thisone')),
+					array(new SOAPAttributeValue('impact', 'our lives'))
+				),
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'caller not specified',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Desktop', /* sType */
+			'PC burning', /* sDescription */
+			'The power supply suddenly started to warm up', /* sInitialSituation */
+			'The agent could not do his job', /* sImpact */
+			null, /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'wrong condition on CI to attach',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Desktop', /* sType */
+			'PC burning', /* sDescription */
+			'The power supply suddenly started to warm up', /* sInitialSituation */
+			'The agent could not do his job', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+				new SOAPLinkCreationSpec(
+					'logInfra',
+					array(new SOAPSearchCondition('dummyfiltercode', 2)),
+					array(new SOAPAttributeValue('impact', 'very critical'))
+				),
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => true,
+		'explain result' => 'no CI to attach (empty array)',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Network', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => true,
+		'explain result' => 'no CI to attach (null)',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Network', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			null, /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'caller unknown',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'Network', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1000))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'wrong values for type and severity',
+		'args' => array(
+			'admin', /* sLogin */
+			'admin', /* sPassword */
+			'my type', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'my severity' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'wrong password',
+		'args' => array(
+			'admin', /* sLogin */
+			'xxxxx', /* sPassword */
+			'Network', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+	array(
+		'verb' => 'CreateIncidentTicket',
+		'expected result' => false,
+		'explain result' => 'wrong login',
+		'args' => array(
+			'xxxxx', /* sLogin */
+			'yyyyy', /* sPassword */
+			'Network', /* sType */
+			'Houston not reachable', /* sDescription */
+			'Tried to join the shuttle', /* sInitialSituation */
+			'Could not talk to my wife', /* sImpact */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 1))), /* aCallerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('id', 2))), /* aCustomerDesc */
+			new SOAPExternalKeySearch(array(new SOAPSearchCondition('name', 'FLS Desktop'))), /* aWorkgroupDesc */
+			array(
+			), /* aImpact */
+			'low' /* sSeverity */
+		),
+	),
+);
+
+
+class TestSoap extends TestSoapWebService
+{
+	static public function GetName() {return 'Test SOAP';}
+	static public function GetDescription() {return 'Do basic stuff to test the SOAP capability';}
+
+	protected function DoExecute()
+	{
+		echo "<p>Note: You may also want to try the sample SOAP client <a href=\"../webservices/itopsoap.examples.php\">itopsoap.examples.php</a></p>\n";
+
+		global $aSOAPMapping;
+
+		// this file is generated dynamically with location = here
+		$sWsdlUri = 'http'.(empty($_SERVER['HTTPS']) ? '' : 's').'://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].dirname($_SERVER['SCRIPT_NAME']).'/../webservices/itop.wsdl.php';
+
+		ini_set("soap.wsdl_cache_enabled","0");
+		$this->m_SoapClient = new SoapClient
+		(
+			$sWsdlUri,
+			array(
+				'classmap' => $aSOAPMapping,
+				'trace' => 1,
+			)
+		);
+
+		if (false)
+		{
+			print "<pre>\n"; 
+			print_r($this->m_SoapClient->__getTypes());
+			print "</pre>\n";
+		} 
+
+		global $aWebServices;
+		foreach ($aWebServices as $iPos => $aWebService)
+		{
+			echo "<h4>SOAP call #$iPos ".$aWebService['explain result']."</h4>\n";
+
+			try
+			{
+				$oRes = call_user_func_array(array($this->m_SoapClient, $aWebService['verb']), $aWebService['args']);
+			}
+			catch(SoapFault $e)
+			{
+				print "<pre>\n"; 
+				print "Request: \n".htmlspecialchars($this->m_SoapClient->__getLastRequest()) ."\n"; 
+				print "Response: \n".htmlspecialchars($this->m_SoapClient->__getLastResponse())."\n"; 
+				print "</pre>";
+				print "Response in HTML: <p>".$this->m_SoapClient->__getLastResponse()."</p>"; 
+				throw $e;
+			}
+
+			echo "<pre>\n";
+			print_r($oRes);
+			echo "</pre>\n";
+	
+			print "<pre>\n"; 
+			print "Request: \n".htmlspecialchars($this->m_SoapClient->__getLastRequest()) ."\n"; 
+			print "Response: \n".htmlspecialchars($this->m_SoapClient->__getLastResponse())."\n"; 
+			print "</pre>";
+
+			if ($oRes instanceof SOAPResult)
+			{
+				$res = $oRes->status;
+			}
+			else
+			{
+				$res = $oRes;
+			}
+			if ($res != $aWebService['expected result'])
+			{
+				throw new UnitTestException("Expecting result '{$aWebService['expected result']}', but got '$res'");
+			}
+		} 
+	}
+}
+
+class TestWebServicesDirect extends TestBizModel
+{
+	static public function GetName() {return 'Test web services locally';}
+	static public function GetDescription() {return 'Invoke the service directly (troubleshooting)';}
+
+	static public function GetConfigFile() {return '../config-itop.php';}
+
+	protected function DoExecute()
+	{
+		$oWebServices = new WebServices();
+
+		global $aWebServices;
+		foreach ($aWebServices as $aWebService)
+		{
+			$oRes = call_user_func_array(array($oWebServices, $aWebService['verb']), $aWebService['args']);
+			echo "<pre>\n";
+			print_r($oRes);
+			echo "</pre>\n";
+		}
+		return true;
+	}
+}
+
+class TestTriggerAndEmail extends TestBizModel
+{
+	static public function GetName() {return 'Test trigger and email';}
+	static public function GetDescription() {return 'Create a trigger and an email, then activates the trigger';}
+
+	static public function GetConfigFile() {return '../config-itop.php';}
+
+	protected function CreateEmailSpec($oTrigger, $sStatus, $sTo, $sCC, $sTesterEmail)
+	{
+		$oAction = MetaModel::NewObject("ActionEmail");
+		$oAction->Set("status", $sStatus);
+		$oAction->Set("name", "New server");
+		$oAction->Set("test_recipient", $sTesterEmail);
+		$oAction->Set("from", $sTesterEmail);
+		$oAction->Set("reply_to", $sTesterEmail);
+		$oAction->Set("to", $sTo);
+		$oAction->Set("cc", $sCC);
+		$oAction->Set("bcc", "");
+		$oAction->Set("subject", "New server: '\$this->name()$'");
+		$oAction->Set("body", "<html><body><p>Dear customer,</p><p>We have created the server \$this->hyperlink()$ in the IT infrastructure database.</p><p>You will be further notified when it is in <strong>Production</strong>.</p><p>The IT infrastructure management team.</p><p>Here are some accentuated characters for french people: 'ééà'</p></body></html>");
+		$oAction->Set("importance", "low");
+		$iActionId = $this->ObjectToDB($oAction, true);
+
+		$oLink = MetaModel::NewObject("lnkTriggerAction");
+		$oLink->Set("trigger_id", $oTrigger->GetKey());
+		$oLink->Set("action_id", $iActionId);
+		$oLink->Set("order", "1");
+		$iLink = $this->ObjectToDB($oLink, true);
+	}
+
+	protected function DoExecute()
+	{
+		$oMyPerson = MetaModel::NewObject("bizPerson");
+		$oMyPerson->Set("name", "testemail1");
+		$oMyPerson->Set("org_id", "1");
+		$oMyPerson->Set("email", "romain.quetiez@hp.com");
+		$iPersonId = $this->ObjectToDB($oMyPerson, true);
+
+		$oMyPerson = MetaModel::NewObject("bizPerson");
+		$oMyPerson->Set("name", "testemail2");
+		$oMyPerson->Set("org_id", "1");
+		$oMyPerson->Set("email", "denis.flaven@hp.com");
+		$iPersonId = $this->ObjectToDB($oMyPerson, true);
+
+		$oMyPerson = MetaModel::NewObject("bizPerson");
+		$oMyPerson->Set("name", "testemail3");
+		$oMyPerson->Set("org_id", "1");
+		$oMyPerson->Set("email", "erwan.taloc@hp.com");
+		$iPersonId = $this->ObjectToDB($oMyPerson, true);
+
+		$oMyServer = MetaModel::NewObject("bizServer");
+		$oMyServer->Set("name", "wfr.terminator.com");
+		$oMyServer->Set("severity", "low");
+		$oMyServer->Set("status", "production");
+		$oMyServer->Set("org_id", 2);
+		$oMyServer->Set("location_id", 2);
+		$iServerId = $this->ObjectToDB($oMyServer, true);
+
+		$oMyTrigger = MetaModel::NewObject("TriggerOnStateEnter");
+		$oMyTrigger->Set("description", "Testor");
+		$oMyTrigger->Set("target_class", "bizServer");
+		$oMyTrigger->Set("state", "Shipped");
+		$iTriggerId = $this->ObjectToDB($oMyTrigger, true);
+
+		// Error in OQL field(s)
+		//
+		$this->CreateEmailSpec
+		(
+			$oMyTrigger,
+			'test',
+			"SELECT bizPerson WHERE naime = 'Dali'",
+			"SELECT bizServer",
+			'romain.quetiez@hp.com'
+		);
+
+		// Error: no recipient
+		//
+		$this->CreateEmailSpec
+		(
+			$oMyTrigger,
+			'test',
+			"",
+			"",
+			'romain.quetiez@hp.com'
+		);
+
+		// Test
+		//
+		$this->CreateEmailSpec
+		(
+			$oMyTrigger,
+			'test',
+			"SELECT bizPerson WHERE name LIKE 'testemail%'",
+			"SELECT bizPerson",
+			'romain.quetiez@hp.com'
+		);
+
+		// Test failing because of a wrong test recipient address
+		//
+		$this->CreateEmailSpec
+		(
+			$oMyTrigger,
+			'test',
+			"SELECT bizPerson WHERE name LIKE 'testemail%'",
+			"",
+			'toto@walibi.bg'
+		);
+
+		// Normal behavior
+		//
+		$this->CreateEmailSpec
+		(
+			$oMyTrigger,
+			'enabled',
+			"SELECT bizPerson WHERE name LIKE 'testemail%'",
+			"",
+			'romain.quetiez@hp.com'
+		);
+
+		// Does nothing, because it is disabled
+		//
+		$this->CreateEmailSpec
+		(
+			$oMyTrigger,
+			'disabled',
+			"SELECT bizPerson WHERE name = 'testemail%'",
+			"",
+			'romain.quetiez@hp.com'
+		);
+
+		$oMyTrigger->DoActivate($oMyServer->ToArgs('this'));
+
+		return true;
+	}
+}
+?>

+ 58 - 43
setup/ajax.dataloader.php

@@ -5,49 +5,64 @@
  * 'file' string Name of the file to load
  * 'session_status' string 'start', 'continue' or 'end'
  * 'percent' integer 0..100 the percentage of completion once the file has been loaded 
- */ 
+ */ 
 define('SAFE_MINIMUM_MEMORY', 32*1024*1024);
 require_once('../application/utils.inc.php');
 require_once('./setuppage.class.inc.php');
-
-$iMemoryLimit = utils::ConvertToBytes(ini_get('memory_limit'));
-if ($iMemoryLimit < SAFE_MINIMUM_MEMORY)
-{
-	if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE)
-	{
-		SetupWebPage::error("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself.");		
-	}
-	else
-	{
-		SetupWebPage::log("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY.".");		
-	}
-}
-
-function FatalErrorCatcher($sOutput)
-{ 
-	if ( preg_match('|<phpfatalerror>.*</phpfatalerror>|s', $sOutput, &$aMatches) )
-	{
+
+$sMemoryLimit = trim(ini_get('memory_limit'));
+if (empty($sMemoryLimit))
+{
+	// On some PHP installations, memory_limit does not exist as a PHP setting!
+	// (encountered on a 5.2.0 under Windows)
+	// In that case, ini_set will not work, let's keep track of this and proceed with the data load
+	SetupWebPage::log_info("No memory limit has been defined in this instance of PHP");		
+}
+else
+{
+	// Check that the limit will allow us to load the data
+	//
+	$iMemoryLimit = utils::ConvertToBytes($sMemoryLimit);
+	if ($iMemoryLimit < SAFE_MINIMUM_MEMORY)
+	{
+		if (ini_set('memory_limit', SAFE_MINIMUM_MEMORY) === FALSE)
+		{
+			SetupWebPage::log_error("memory_limit is too small: $iMemoryLimit and can not be increased by the script itself.");		
+		}
+		else
+		{
+			SetupWebPage::log_info("memory_limit increased from $iMemoryLimit to ".SAFE_MINIMUM_MEMORY.".");		
+		}
+	}
+
+}
+
+
+function FatalErrorCatcher($sOutput)
+{ 
+	if ( preg_match('|<phpfatalerror>.*</phpfatalerror>|s', $sOutput, $aMatches) )
+	{
 		header("HTTP/1.0 500 Internal server error.");
 		foreach ($aMatches as $sMatch)
 		{
 			$errors .= strip_tags($sMatch)."\n";
 		}
-		$sOutput = "$errors\n";
-		// Logging to a file does not work if the whole memory is exhausted...		
-		//SetupWebPage::error("Fatal error - in $__FILE__ , $errors");
-	}
+		$sOutput = "$errors\n";
+		// Logging to a file does not work if the whole memory is exhausted...		
+		//SetupWebPage::log_error("Fatal error - in $__FILE__ , $errors");
+	}
 	return $sOutput;
-}
+}
 	
-//Define some bogus, invalid HTML tags that no sane
-//person would ever put in an actual document and tell
-//PHP to delimit fatal error warnings with them.
+//Define some bogus, invalid HTML tags that no sane
+//person would ever put in an actual document and tell
+//PHP to delimit fatal error warnings with them.
 ini_set('error_prepend_string', '<phpfatalerror>');
 ini_set('error_append_string', '</phpfatalerror>');
-
-// Starts the capture of the ouput, and sets a filter to capture the fatal errors.
-ob_start('FatalErrorCatcher'); // Start capturing the output, and pass it through the fatal error catcher
-
+
+// Starts the capture of the ouput, and sets a filter to capture the fatal errors.
+ob_start('FatalErrorCatcher'); // Start capturing the output, and pass it through the fatal error catcher
+
 require_once('../core/config.class.inc.php');
 require_once('../core/cmdbsource.class.inc.php');
 require_once('./xmldataloader.class.inc.php');
@@ -58,14 +73,14 @@ define('TMP_CONFIG_FILE', '../tmp-config-itop.php');
 // Never cache this page
 header("Cache-Control: no-cache, must-revalidate");  // HTTP/1.1
 header("Expires: Fri, 17 Jul 1970 05:00:00 GMT");    // Date in the past
-
+
 /**
  * Main program
  */
 $sFileName = Utils::ReadParam('file', '');
 $sSessionStatus = Utils::ReadParam('session_status', '');
 $iPercent = (integer)Utils::ReadParam('percent', 0);
-SetupWebPage::log("Info - Loading file: $sFileName");
+SetupWebPage::log_info("Loading file: $sFileName");
 
 try
 {
@@ -81,30 +96,30 @@ try
 		$oChange->Set("date", time());
 		$oChange->Set("userinfo", "Initialization");
 		$iChangeId = $oChange->DBInsert();
-		SetupWebPage::log("Info - starting data load session");
+		SetupWebPage::log_info("starting data load session");
 		$oDataLoader->StartSession($oChange);
 	}
 
 	$oDataLoader->LoadFile($sFileName);
-	$sResult = sprintf("Info - loading of %s done. (Overall %d %% completed).", basename($sFileName), $iPercent);
+	$sResult = sprintf("loading of %s done. (Overall %d %% completed).", basename($sFileName), $iPercent);
 	echo $sResult;
-	SetupWebPage::log($sResult);
+	SetupWebPage::log_info($sResult);
 
 	if ($sSessionStatus == 'end')
 	{
 	    $oDataLoader->EndSession();
-	    SetupWebPage::log("Info - ending data load session");
+	    SetupWebPage::log_info("ending data load session");
 	}
 }
 catch(Exception $e)
 {
 	echo "<p>An error happened while loading the data</p>\n";
 	echo '<p>'.$e."</p>\n";
-	SetupWebPage::log("Error - An error happened while loading the data. ".$e);
-}
-
-if (function_exists('memory_get_peak_usage'))
-{
-	SetupWebPage::log("Info - loading file '$sFileName', peak memory usage. ".memory_get_peak_usage());
+	SetupWebPage::log_error("An error happened while loading the data. ".$e);
+}
+
+if (function_exists('memory_get_peak_usage'))
+{
+	SetupWebPage::log_info("loading file '$sFileName', peak memory usage. ".memory_get_peak_usage());
 }
 ?>

+ 24 - 4
setup/setuppage.class.inc.php

@@ -101,25 +101,25 @@ table.formTable {
 	public function info($sText)
 	{
 		$this->add("<p class=\"info\">$sText</p>\n");
-		$this->log("Info - ".$sText);
+		$this->log_info($sText);
 	}
 	
 	public function ok($sText)
 	{
 		$this->add("<p class=\"ok\">$sText</p>\n");
-		$this->log("Ok - ".$sText);
+		$this->log_ok($sText);
 	}
 	
 	public function warning($sText)
 	{
 		$this->add("<p class=\"warning\">$sText</p>\n");
-		$this->log("Warning - ".$sText);
+		$this->log_warning($sText);
 	}
 	
 	public function error($sText)
 	{
 		$this->add("<p class=\"error\">$sText</p>\n");
-		$this->log("Error - ".$sText);
+		$this->log_error($sText);
 	}
 	
 	public function form($aData)
@@ -159,6 +159,26 @@ table.formTable {
 		return parent::output();
 	}
 	
+	public static function log_error($sText)
+	{
+		self::log("Error - ".$sText);
+	}
+
+	public static function log_warning($sText)
+	{
+		self::log("Warning - ".$sText);
+	}
+
+	public static function log_info($sText)
+	{
+		self::log("Info - ".$sText);
+	}
+
+	public static function log_ok($sText)
+	{
+		self::log("Ok - ".$sText);
+	}
+
 	public static function log($sText)
 	{
 		$hLogFile = @fopen(INSTALL_LOG_FILE, 'a');

+ 3 - 3
setup/xmldataloader.class.inc.php

@@ -177,7 +177,7 @@ class XMLDataLoader
 						// tested by Romain, little impact on perf (not significant on the intial setup)
 						if (!$oTargetObj->CheckValue($sAttCode, (string)$oXmlObj->$sAttCode))
 						{
-							SetupWebPage::log("Error - Value not allowed - $sClass/$iSrcId - $sAttCode: '".$oXmlObj->$sAttCode."'");
+							SetupWebPage::log_error("Value not allowed - $sClass/$iSrcId - $sAttCode: '".$oXmlObj->$sAttCode."'");
 							echo "Wrong value for attribute $sAttCode: '".$oXmlObj->$sAttCode."'";
 						}
 						$oTargetObj->Set($sAttCode, (string)$oXmlObj->$sAttCode);
@@ -223,7 +223,7 @@ class XMLDataLoader
 		}
 		catch(Exception $e)
 		{
-			SetupWebPage::log("Error - An object could not be loaded - $sClass/$iSrcId - ".$e->getMessage());
+			SetupWebPage::log_error("An object could not be loaded - $sClass/$iSrcId - ".$e->getMessage());
 			echo $e->GetHtmlDesc();
 		}
 		$aParentClasses = MetaModel::EnumParentClasses($sClass);
@@ -257,7 +257,7 @@ class XMLDataLoader
 						if ($iExtKey == 0)
 						{
 							$sMsg = "unresolved extkey in $sClass::".$oTargetObj->GetKey()."(".$oTargetObj->GetName().")::$sAttCode=$sTargetClass::$iTempKey";
-							SetupWebPage::log("Warning - $sMsg");
+							SetupWebPage::log_warning($sMsg);
 							echo "Warning: $sMsg<br/>\n";
 							echo "<pre>aKeys[".$sTargetClass."]:\n";
 							print_r($this->m_aKeys[$sTargetClass]);

+ 3 - 7
webservices/import.php

@@ -18,6 +18,7 @@
 // - only external fields attributes could be used as reconciliation keys for external keys
 // - reconciliation is made on the first column
 // - no option to force 'always create' or 'never create'
+// - text qualifier hardcoded to "
 //
 // Known issues
 // - ALMOST impossible to troubleshoot when an externl key has a wrong value
@@ -58,11 +59,9 @@ try
 	$sSep = utils::ReadParam('separator', ';');
 	$sCSVData = utils::ReadPostedParam('csvdata');
 
-	$oCSVParser = new CSVParser($sCSVData); 
-	$oCSVParser->SetSeparator($sSep);
-	$oCSVParser->SetSkipLines(1);
+	$oCSVParser = new CSVParser($sCSVData, $sSep, $sDelimiter = '"'); 
 
-	// Limitation: as the attribute list is in the first line, we can not match external key by an third-party attribute
+	// Limitation: as the attribute list is in the first line, we can not match external key by a third-party attribute
 	$sRawFieldList = $oCSVParser->ListFields();
 	$aAttList = array();
 	$aExtKeys = array();
@@ -93,9 +92,6 @@ try
 	// Limitation: the reconciliation key is the first attribute
 	$aReconcilKeys = array($sRawFieldList[0]);
 
-//	print_r($oCSVParser->ListFields());
-//	print_r($oCSVParser->ToArray($oCSVParser->ListFields()));
-
 	$aData = $oCSVParser->ToArray();
 	$oBulk = new BulkChange(
 		$sClass,