Browse Source

Data Exchange - Improved to allow the import of complex attributes like passwords and documents + few fixes

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1106 a333f486-631f-4898-b8df-5754b55c2be0
romainq 14 năm trước cách đây
mục cha
commit
235777e192

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

@@ -204,12 +204,27 @@ abstract class AttributeDefinition
 
 	public function MakeRealValue($proposedValue) {return $proposedValue;} // force an allowed value (type conversion and possibly forces a value as mySQL would do upon writing!)
 
-	public function GetSQLExpressions() {return array();} // returns suffix/expression pairs (1 in most of the cases), for READING (Select)
+	public function GetSQLExpressions($sPrefix = '') {return array();} // returns suffix/expression pairs (1 in most of the cases), for READING (Select)
 	public function FromSQLToValue($aCols, $sPrefix = '') {return null;} // returns a value out of suffix/value pairs, for SELECT result interpretation
 	public function GetSQLColumns() {return array();} // returns column/spec pairs (1 in most of the cases), for STRUCTURING (DB creation)
 	public function GetSQLValues($value) {return array();} // returns column/value pairs (1 in most of the cases), for WRITING (Insert, Update)
 	public function RequiresIndex() {return false;}
 
+   // Import - differs slightly from SQL input, but identical in most cases
+   //
+	public function GetImportColumns() {return $this->GetSQLColumns();}
+	public function FromImportToValue($aCols, $sPrefix = '')
+	{
+		$aValues = array();
+		foreach ($this->GetSQLExpressions($sPrefix) as $sAlias => $sExpr)
+		{
+			// This is working, based on the assumption that importable fields
+			// are not computed fields => the expression is the name of a column
+			$aValues[$sPrefix.$sAlias] = $aCols[$sExpr];
+		}
+		return $this->FromSQLToValue($aValues, $sPrefix);
+	}
+
 	public function GetValidationPattern()
 	{
 		return '';
@@ -414,7 +429,7 @@ class AttributeDBFieldVoid extends AttributeDefinition
 	// 
 	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
 
-	public function GetSQLExpressions()
+	public function GetSQLExpressions($sPrefix = '')
 	{
 		$aColumns = array();
 		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
@@ -1941,9 +1956,16 @@ class AttributeExternalField extends AttributeDefinition
 		return $oExtAttDef->GetSQLCol(); 
 	}
 
-	public function GetSQLExpressions()
+	public function GetSQLExpressions($sPrefix = '')
 	{
-		return array('' => $this->GetCode()); 
+		if ($sPrefix == '')
+		{
+			return array('' => $this->GetCode());
+		}
+		else
+		{
+			return $sPrefix;
+		} 
 	}
 
 	public function GetLabel()
@@ -2096,7 +2118,7 @@ class AttributeExternalField extends AttributeDefinition
 
 
 	// Do not overload GetSQLExpression here because this is handled in the joins
-	//public function GetSQLExpressions() {return array();}
+	//public function GetSQLExpressions($sPrefix = '') {return array();}
 
 	// Here, we get the data...
 	public function FromSQLToValue($aCols, $sPrefix = '')
@@ -2187,13 +2209,17 @@ class AttributeBlob extends AttributeDefinition
 		return $proposedValue;
 	}
 
-	public function GetSQLExpressions()
+	public function GetSQLExpressions($sPrefix = '')
 	{
+		if ($sPrefix == '')
+		{
+			$sPrefix = $this->GetCode();
+		}
 		$aColumns = array();
 		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
-		$aColumns[''] = $this->GetCode().'_mimetype';
-		$aColumns['_data'] = $this->GetCode().'_data';
-		$aColumns['_filename'] = $this->GetCode().'_filename';
+		$aColumns[''] = $sPrefix.'_mimetype';
+		$aColumns['_data'] = $sPrefix.'_data';
+		$aColumns['_filename'] = $sPrefix.'_filename';
 		return $aColumns;
 	}
 
@@ -2331,12 +2357,16 @@ class AttributeOneWayPassword extends AttributeDefinition
 		return $oPassword;
 	}
 
-	public function GetSQLExpressions()
+	public function GetSQLExpressions($sPrefix = '')
 	{
+		if ($sPrefix == '')
+		{
+			$sPrefix = $this->GetCode();
+		}
 		$aColumns = array();
 		// Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix
-		$aColumns[''] = $this->GetCode().'_hash';
-		$aColumns['_salt'] = $this->GetCode().'_salt';
+		$aColumns[''] = $sPrefix.'_hash';
+		$aColumns['_salt'] = $sPrefix.'_salt';
 		return $aColumns;
 	}
 
@@ -2391,6 +2421,27 @@ class AttributeOneWayPassword extends AttributeDefinition
 		return $aColumns;
 	}
 
+	public function GetImportColumns()
+	{
+		$aColumns = array();
+		$aColumns[$this->GetCode()] = 'TINYTEXT';
+		return $aColumns;
+	}
+
+	public function FromImportToValue($aCols, $sPrefix = '')
+	{
+		if (!isset($aCols[$sPrefix]))
+		{
+			$sAvailable = implode(', ', array_keys($aCols));
+			throw new MissingColumnException("Missing column '$sPrefix' from {$sAvailable}");
+		} 
+		$sClearPwd = $aCols[$sPrefix];
+
+		$oPassword = new ormPassword('', '');
+		$oPassword->SetPassword($sClearPwd);
+		return $oPassword;
+	}
+
 	public function GetFilterDefinitions()
 	{
 		return array();
@@ -2574,9 +2625,13 @@ class AttributeComputedFieldVoid extends AttributeDefinition
 	// 
 //	protected function ScalarToSQL($value) {return $value;} // format value as a valuable SQL literal (quoted outside)
 
-	public function GetSQLExpressions()
+	public function GetSQLExpressions($sPrefix = '')
 	{
-		return array('' => $this->GetCode()); 
+		if ($sPrefix == '')
+		{
+			$sPrefix = $this->GetCode();
+		}
+		return array('' => $sPrefix); 
 	}
 
 	public function FromSQLToValue($aCols, $sPrefix = '')

+ 1 - 1
core/config.class.inc.php

@@ -94,7 +94,7 @@ class Config
 		),
 		'skip_check_ext_keys' => array(
 			'type' => 'bool',
-			'description' => 'Disable external key check when checking the value of attribtutes',
+			'description' => 'Disable external key check when checking the value of attributes',
 			'default' => false,
 			'value' => false,
 			'source_of_value' => '',

+ 15 - 13
core/dbobject.class.php

@@ -396,23 +396,25 @@ abstract class DBObject
 		}
 	}
 	
+	public function ComputeValues()
+	{
+	}
+
 	// Compute scalar attributes that depend on any other type of attribute
-	public function DoComputeValues()
+	final public function DoComputeValues()
 	{
-		if (is_callable(array($this, 'ComputeValues')))
+		// TODO - use a flag rather than checking the call stack -> this will certainly accelerate things
+
+		// First check that we are not currently computing the fields
+		// (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...)
+		foreach (debug_backtrace() as $aCallInfo)
 		{
-			// First check that we are not currently computing the fields
-			// (yes, we need to do some things like Set/Get to compute the fields which will in turn trigger the update...)
-			foreach (debug_backtrace() as $aCallInfo)
-			{
-				if (!array_key_exists("class", $aCallInfo)) continue;
-				if ($aCallInfo["class"] != get_class($this)) continue;
-				if ($aCallInfo["function"] != "ComputeValues") continue;
-				return; //skip!
-			}
-			
-			$this->ComputeValues();
+			if (!array_key_exists("class", $aCallInfo)) continue;
+			if ($aCallInfo["class"] != get_class($this)) continue;
+			if ($aCallInfo["function"] != "ComputeValues") continue;
+			return; //skip!
 		}
+		$this->ComputeValues();
 	}
 
 	public function GetAsHTML($sAttCode)

+ 20 - 9
core/metamodel.class.php

@@ -1657,18 +1657,29 @@ abstract class MetaModel
 		}
 		return $aSubClasses;
 	}
-	public static function GetClasses($sCategory = '')
+	public static function GetClasses($sCategories = '', $bStrict = false)
 	{
-		if (array_key_exists($sCategory, self::$m_Category2Class))
+		$aCategories = explode(',', $sCategories);
+		$aClasses = array();
+		foreach($aCategories as $sCategory)
 		{
-			return self::$m_Category2Class[$sCategory];
-		}
+			$sCategory = trim($sCategory);
+			if (strlen($sCategory) == 0)
+			{
+				return array_keys(self::$m_aClassParams);
+			}
 
-		//if (count(self::$m_Category2Class) > 0)
-		//{
-		//	throw new CoreException("unkown class category '$sCategory', expecting a value in {".implode(', ', array_keys(self::$m_Category2Class))."}");
-		//}
-		return array();
+			if (array_key_exists($sCategory, self::$m_Category2Class))
+			{
+				$aClasses = array_merge($aClasses, self::$m_Category2Class[$sCategory]);
+			}
+			elseif ($bStrict)
+			{
+				throw new CoreException("unkown class category '$sCategory', expecting a value in {".implode(', ', array_keys(self::$m_Category2Class))."}");
+			}
+		}
+		
+		return array_unique($aClasses);
 	}
 
 	public static function HasTable($sClass)

+ 1 - 1
dictionaries/dictionary.itop.core.php

@@ -537,7 +537,7 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Core:SynchroLogTitle' => '%1$s - %2$s',
 	'Core:Synchro:Nb_Replica' => 'Replica processed: %1$s',
 	'Core:Synchro:Nb_Class:Objects' => '%1$s: %2$s',
-	'Class:SynchroDataSource/Error:AtLeastOneReconciliationKeyMustBeSpecified' => 'At Least one reconciliation key must be specified.',			
+	'Class:SynchroDataSource/Error:AtLeastOneReconciliationKeyMustBeSpecified' => 'At Least one reconciliation key must be specified, or the reconciliation policy must be to use the primary key.',			
 	'Class:SynchroDataSource/Error:DeleteRetentionDurationMustBeSpecified' => 'A delete retention period must be specified, since objects are to be deleted after being marked as obsolete',			
 	'Class:SynchroDataSource/Error:DeletePolicyUpdateMustBeSpecified' => 'Obsolete objects are to be updated, but no update is specified.',
 	'Core:SynchroReplica:PublicData' => 'Public Data',

+ 1 - 1
modules/authent-external/model.authent-external.php

@@ -40,7 +40,7 @@ class UserExternal extends User
 			"key_type" => "autoincrement",
 			"name_attcode" => "login",
 			"state_attcode" => "",
-			"reconc_keys" => array(),
+			"reconc_keys" => array('login'),
 			"db_table" => "",
 			"db_key_field" => "id",
 			"db_finalclass_field" => "",

+ 1 - 1
modules/authent-ldap/model.authent-ldap.php

@@ -35,7 +35,7 @@ class UserLDAP extends UserInternal
 			"key_type" => "autoincrement",
 			"name_attcode" => "login",
 			"state_attcode" => "",
-			"reconc_keys" => array(),
+			"reconc_keys" => array('login'),
 			"db_table" => "",
 			"db_key_field" => "id",
 			"db_finalclass_field" => "",

+ 1 - 1
modules/authent-local/model.authent-local.php

@@ -35,7 +35,7 @@ class UserLocal extends UserInternal
 			"key_type" => "autoincrement",
 			"name_attcode" => "login",
 			"state_attcode" => "",
-			"reconc_keys" => array(),
+			"reconc_keys" => array('login'),
 			"db_table" => "priv_user_local",
 			"db_key_field" => "id",
 			"db_finalclass_field" => "",

+ 117 - 80
synchro/synchrodatasource.class.inc.php

@@ -52,7 +52,7 @@ class SynchroDataSource extends cmdbAbstractObject
 		MetaModel::Init_AddAttribute(new AttributeString("description", array("allowed_values"=>null, "sql"=>"description", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('implementation,production,obsolete'), "sql"=>"status", "default_value"=>"implementation", "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=>null, "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeClass("scope_class", array("class_category"=>"bizmodel", "more_values"=>"", "sql"=>"scope_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeClass("scope_class", array("class_category"=>"bizmodel,addon/authentication", "more_values"=>"", "sql"=>"scope_class", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
 		
 		// Declared here for a future usage, but ignored so far
 		MetaModel::Init_AddAttribute(new AttributeString("scope_restriction", array("allowed_values"=>null, "sql"=>"scope_restriction", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
@@ -60,7 +60,7 @@ class SynchroDataSource extends cmdbAbstractObject
 		//MetaModel::Init_AddAttribute(new AttributeDateTime("last_synchro_date", array("allowed_values"=>null, "sql"=>"last_synchro_date", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
 
 		// Format: seconds (int)
-		MetaModel::Init_AddAttribute(new AttributeDuration("full_load_periodicity", array("allowed_values"=>null, "sql"=>"full_load_periodicity", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeDuration("full_load_periodicity", array("allowed_values"=>null, "sql"=>"full_load_periodicity", "default_value"=>86400, "is_null_allowed"=>true, "depends_on"=>array())));
 		
 //		MetaModel::Init_AddAttribute(new AttributeString("reconciliation_list", array("allowed_values"=>null, "sql"=>"reconciliation_list", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("reconciliation_policy", array("allowed_values"=>new ValueSetEnum('use_primary_key,use_attributes'), "sql"=>"reconciliation_policy", "default_value"=>"use_attributes", "is_null_allowed"=>false, "depends_on"=>array())));
@@ -433,9 +433,9 @@ EOF
 	/*
 	* Overload the standard behavior
 	*/	
-	public function DoCheckToWrite()
+	public function ComputeValues()
 	{
-		parent::DoCheckToWrite();
+		parent::ComputeValues();
 
 		if ($this->IsNew())
 		{
@@ -467,6 +467,10 @@ EOF
 			}
 			$this->Set('attribute_list', $oAttributeSet);
 		}
+	}
+	public function DoCheckToWrite()
+	{
+		parent::DoCheckToWrite();
 
 		// Check that there is at least one reconciliation key defined
 		if ($this->Get('reconciliation_policy') == 'use_attributes')
@@ -676,6 +680,70 @@ EOF
 			throw new SynchroExceptionNotStarted(Dict::S('Core:SyncDataSourceAccessRestriction'));
 		}
 
+		// Get the list of SQL columns
+		$sClass = $this->GetTargetClass();
+		$aAttCodesExpected = array();
+		$aAttCodesToReconcile = array();
+		$aAttCodesToUpdate = array();
+		$sSelectAtt  = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND (update = 1 OR reconcile = 1)";
+		$oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
+		while ($oSyncAtt = $oSetAtt->Fetch())
+		{
+			if ($oSyncAtt->Get('update'))
+			{
+				$aAttCodesToUpdate[$oSyncAtt->Get('attcode')] = $oSyncAtt;
+			}
+			if ($oSyncAtt->Get('reconcile'))
+			{
+				$aAttCodesToReconcile[$oSyncAtt->Get('attcode')] = $oSyncAtt;
+			}
+			$aAttCodesExpected[$oSyncAtt->Get('attcode')] = $oSyncAtt;
+		}
+		$aColumns = $this->GetSQLColumns(array_keys($aAttCodesExpected));
+		$aExtDataFields = array_keys($aColumns);
+		$aExtDataFields[] = 'primary_key';
+		$aExtDataSpec = array(
+			'table' => $this->GetDataTable(),
+			'join_key' => 'id',
+			'fields' => $aExtDataFields
+		);
+
+		// Get the list of attributes, determine reconciliation keys and update targets
+		//
+		if ($this->Get('reconciliation_policy') == 'use_attributes')
+		{
+			$aReconciliationKeys = $aAttCodesToReconcile;
+		}
+		elseif ($this->Get('reconciliation_policy') == 'use_primary_key')
+		{
+			// Override the setings made at the attribute level !
+			$aReconciliationKeys = array("primary_key" => null);
+		}
+
+		$oStatLog->AddTrace("Update of: {".implode(', ', array_keys($aAttCodesToUpdate))."}");
+		$oStatLog->AddTrace("Reconciliation on: {".implode(', ', array_keys($aReconciliationKeys))."}");
+
+		if (count($aAttCodesToUpdate) == 0)
+		{
+			$oStatLog->AddTrace("No attribute to update");
+			throw new SynchroExceptionNotStarted('There is no attribute to update');
+		}
+		if (count($aReconciliationKeys) == 0)
+		{
+			$oStatLog->AddTrace("No attribute for reconciliation");
+			throw new SynchroExceptionNotStarted('No attribute for reconciliation');
+		}
+		
+		$aAttributes = array();
+		foreach($aAttCodesToUpdate as $sAttCode => $oSyncAtt)
+		{
+			$oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
+			if ($oAttDef->IsWritable() && $oAttDef->IsScalar())
+			{
+				$aAttributes[$sAttCode] = $oSyncAtt;
+			}
+		}
+
 		$sDeletePolicy = $this->Get('delete_policy');
 
 		// Count the replicas
@@ -742,58 +810,6 @@ EOF
 		
 		// Get all the replicas that are 'new' or modified
 		//
-		// Get the list of SQL columns
-		$sClass = $this->GetTargetClass();
-		$aAttCodesExpected = array();
-		$aAttCodesToReconcile = array();
-		$aAttCodesToUpdate = array();
-		$sSelectAtt  = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND (update = 1 OR reconcile = 1)";
-		$oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */);
-		while ($oSyncAtt = $oSetAtt->Fetch())
-		{
-			if ($oSyncAtt->Get('update'))
-			{
-				$aAttCodesToUpdate[$oSyncAtt->Get('attcode')] = $oSyncAtt;
-			}
-			if ($oSyncAtt->Get('reconcile'))
-			{
-				$aAttCodesToReconcile[$oSyncAtt->Get('attcode')] = $oSyncAtt;
-			}
-			$aAttCodesExpected[$oSyncAtt->Get('attcode')] = $oSyncAtt;
-		}
-		$aColumns = $this->GetSQLColumns(array_keys($aAttCodesExpected));
-		$aExtDataFields = array_keys($aColumns);
-		$aExtDataFields[] = 'primary_key';
-		$aExtDataSpec = array(
-			'table' => $this->GetDataTable(),
-			'join_key' => 'id',
-			'fields' => $aExtDataFields
-		);
-
-		// Get the list of reconciliation keys
-		if ($this->Get('reconciliation_policy') == 'use_attributes')
-		{
-			$aReconciliationKeys = $aAttCodesToReconcile;
-		}
-		elseif ($this->Get('reconciliation_policy') == 'use_primary_key')
-		{
-			// Override the setings made at the attribute level !
-			$aReconciliationKeys = array("primary_key" => null);
-		}
-
-		$oStatLog->AddTrace("Update of: {".implode(', ', array_keys($aAttCodesToUpdate))."}");
-		$oStatLog->AddTrace("Reconciliation on: {".implode(', ', array_keys($aReconciliationKeys))."}");
-		
-		$aAttributes = array();
-		foreach($aAttCodesToUpdate as $sAttCode => $oSyncAtt)
-		{
-			$oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
-			if ($oAttDef->IsWritable() && $oAttDef->IsScalar())
-			{
-				$aAttributes[$sAttCode] = $oSyncAtt;
-			}
-		}
-		
 		$sSelectToSync  = "SELECT SynchroReplica WHERE (status = 'new' OR status = 'modified') AND sync_source_id = :source_id";
 		$oSetToSync = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */, $aExtDataSpec, 0 /* limitCount */, 0 /* limitStart */);
 
@@ -855,7 +871,7 @@ EOF
 			}
 			else
 			{
-				foreach($oAttDef->GetSQLColumns() as $sField => $sDBFieldType)
+				foreach($oAttDef->GetImportColumns() as $sField => $sDBFieldType)
 				{
 					$aColumns[$sField] = $sDBFieldType;
 				}
@@ -1181,8 +1197,10 @@ class SynchroReplica extends DBObject implements iDisplay
 		MetaModel::Init_InheritAttributes();
 
 		MetaModel::Init_AddAttribute(new AttributeExternalKey("sync_source_id", array("targetclass"=>"SynchroDataSource", "jointype"=> "", "allowed_values"=>null, "sql"=>"sync_source_id", "is_null_allowed"=>false, "on_target_delete"=>DEL_AUTO, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeExternalField("base_class", array("allowed_values"=>null, "extkey_attcode"=> 'sync_source_id', "target_attcode"=>"scope_class")));
+
 		MetaModel::Init_AddAttribute(new AttributeInteger("dest_id", array("allowed_values"=>null, "sql"=>"dest_id", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
-		MetaModel::Init_AddAttribute(new AttributeClass("dest_class", array("class_category"=>"bizmodel", "more_values"=>"", "sql"=>"dest_class", "default_value"=>'Organization', "is_null_allowed"=>true, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeClass("dest_class", array("class_category"=>"", "more_values"=>"", "sql"=>"dest_class", "default_value"=>'Organization', "is_null_allowed"=>true, "depends_on"=>array())));
 
 		MetaModel::Init_AddAttribute(new AttributeDateTime("status_last_seen", array("allowed_values"=>null, "sql"=>"status_last_seen", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
 		MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('new,synchronized,modified,orphan,obsolete'), "sql"=>"status", "default_value"=>"new", "is_null_allowed"=>false, "depends_on"=>array())));
@@ -1247,6 +1265,7 @@ class SynchroReplica extends DBObject implements iDisplay
 			// If needed, construct the query used for the reconciliation
 			if (!isset(self::$aSearches[$oDataSource->GetKey()]))
 			{
+				$aCriterias = array();
 				foreach($aReconciliationKeys as $sFilterCode => $oSyncAtt)
 				{
 					$aCriterias[] = ($sFilterCode == 'primary_key' ? 'id' : $sFilterCode).' = :'.$sFilterCode;
@@ -1498,44 +1517,62 @@ class SynchroReplica extends DBObject implements iDisplay
 	/**
 	 * Get the value from the 'Extended Data' located in the synchro_data_xxx table for this replica
 	 */
-	 protected function GetValueFromExtData($sColumnName, $oSyncAtt, &$oStatLog)
-	 {
-	 	// $aData should contain attributes defined either for reconciliation or create/update
+	protected function GetValueFromExtData($sAttCode, $oSyncAtt, &$oStatLog)
+	{
+		// $aData should contain attributes defined either for reconciliation or create/update
 		$aData = $this->GetExtendedData();
 
-      // In any case, a null column means "ignore this column"
-      //
-		if (is_null($aData[$sColumnName]))
-      {
-      	return null;
-		}
+		$sClass = $this->Get('base_class');
+		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
 
 		if (!is_null($oSyncAtt) && ($oSyncAtt instanceof SynchroAttExtKey))
 		{
+			$rawValue = $aData[$sAttCode];
+			if (is_null($rawValue))
+			{
+				// Null means "ignore" this attribute
+				return null;
+			}
+
 			$sReconcAttCode = $oSyncAtt->Get('reconciliation_attcode');
 			if (!empty($sReconcAttCode))
 			{
-				$oDataSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'));
-			 	$sClass = $oDataSource->GetTargetClass();
-		 		$oAttDef = MetaModel::GetAttributeDef($sClass, $sColumnName);
-		 		$sRemoteClass = $oAttDef->GetTargetClass();
-				$oObj = MetaModel::GetObjectByColumn($sRemoteClass, $sReconcAttCode, $aData[$sColumnName], false);
-		 		if ($oObj)
-		 		{
-					 return $oObj->GetKey();
+				$sRemoteClass = $oAttDef->GetTargetClass();
+				$oObj = MetaModel::GetObjectByColumn($sRemoteClass, $sReconcAttCode, $rawValue, false);
+				if ($oObj)
+				{
+					 $retValue = $oObj->GetKey();
 				}
 				else
 				{
 					// Note: differs from null (in which case the value would be left unchanged)
-					$oStatLog->AddTrace("Could not find [unique] object for '$sColumnName': searched on $sReconcAttCode = '$aData[$sColumnName]'", $this);
-					return 0;
+					$oStatLog->AddTrace("Could not find [unique] object for '$sAttCode': searched on $sReconcAttCode = '$rawValue'", $this);
+					$retValue = 0;
+				}
+			}
+			else
+			{
+				$retValue = $rawValue;
+			}
+		}
+		else
+		{
+			$aColumns = $oAttDef->GetImportColumns();
+			foreach($aColumns as $sColumn => $sFormat)
+			{
+				// In any case, a null column means "ignore this attribute"
+				//
+				if (is_null($aData[$sColumn]))
+				{
+					return null;
 				}
 			}
+			$retValue = $oAttDef->FromImportToValue($aData, $sAttCode);
 		}
 
- 		return $aData[$sColumnName];
-	 }
-	 
+		return $retValue;
+	}
+	
 	/**
 	 * Maps the given context parameter name to the appropriate filter/search code for this class
 	 * @param string $sContextParam Name of the context parameter, i.e. 'org_id'