diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 078de5cad..72a6e1fbf 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -827,7 +827,11 @@ EOF { $sSeparator = isset($aParams['separator']) ? $aParams['separator'] : ','; // default separator is comma $sTextQualifier = isset($aParams['text_qualifier']) ? $aParams['text_qualifier'] : '"'; // default text qualifier is double quote - $aFields = isset($aParams['fields']) ? explode(',', $aParams['fields']) : null; + $aFields = null; + if (isset($aParams['fields']) && (strlen($aParams['fields']) > 0)) + { + $aFields = explode(',', $aParams['fields']); + } $aList = array(); @@ -847,11 +851,21 @@ EOF { foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef) { - if (!is_null($aFields) && !in_array($sAttCode, $aFields)) continue; - - if ($oAttDef->IsExternalField() || $oAttDef->IsWritable()) + if (is_null($aFields) || (count($aFields) == 0)) { - $aList[$sClassName][$sAttCode] = $oAttDef; + // Standard list of attributes (no link sets) + if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField())) + { + $aList[$sClassName][$sAttCode] = $oAttDef; + } + } + else + { + // User defined list of attributes + if (in_array($sAttCode, $aFields)) + { + $aList[$sClassName][$sAttCode] = $oAttDef; + } } } $aHeader[] = 'id'; diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index fcd71ec91..dca83f427 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -203,6 +203,7 @@ abstract class AttributeDefinition public function IsNull($proposedValue) {return is_null($proposedValue);} 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 Equals($val1, $val2) {return ($val1 == $val2);} 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 @@ -411,10 +412,19 @@ class AttributeLinkedSet extends AttributeDefinition $aItems = array(); while ($oObj = $sValue->Fetch()) { + $sObjClass = get_class($oObj); // Show only relevant information (hide the external key to the current object) $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) + foreach(MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) { + if ($sAttCode == 'finalclass') + { + if ($sObjClass == $this->GetLinkedClass()) + { + // Simplify the output if the exact class could be determined implicitely + continue; + } + } if ($sAttCode == $this->GetExtKeyToMe()) continue; if ($oAttDef->IsExternalField()) continue; if (!$oAttDef->IsDirectField()) continue; @@ -472,9 +482,9 @@ class AttributeLinkedSet extends AttributeDefinition $aLinks = array(); foreach($aInput as $aRow) { - $aNewRow = array(); - $oLink = MetaModel::NewObject($sTargetClass); + // 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value $aExtKeys = array(); + $aValues = array(); foreach($aRow as $sCell) { $iSepPos = strpos($sCell, $sSepValue); @@ -509,10 +519,35 @@ class AttributeLinkedSet extends AttributeDefinition { throw new CoreException('Wrong attribute code for link attribute specification', array('class' => $sTargetClass, 'attcode' => $sAttCode)); } - $oLink->Set($sAttCode, $sValue); + $aValues[$sAttCode] = $sValue; } } - // Set external keys from search conditions + + // 2nd - Instanciate the object and set the value + if (isset($aValues['finalclass'])) + { + $sLinkClass = $aValues['finalclass']; + if (!is_subclass_of($sLinkClass, $sTargetClass)) + { + throw new CoreException('Wrong class for link attribute specification', array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass)); + } + } + elseif (MetaModel::IsAbstract($sTargetClass)) + { + throw new CoreException('Missing finalclass for link attribute specification'); + } + else + { + $sLinkClass = $sTargetClass; + } + + $oLink = MetaModel::NewObject($sLinkClass); + foreach ($aValues as $sAttCode => $sValue) + { + $oLink->Set($sAttCode, $sValue); + } + + // 3rd - Set external keys from search conditions foreach ($aExtKeys as $sKeyAttCode => $aReconciliation) { $oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode); @@ -547,6 +582,25 @@ class AttributeLinkedSet extends AttributeDefinition $oSet = DBObjectSet::FromArray($sTargetClass, $aLinks); return $oSet; } + + public function Equals($val1, $val2) + { + if ($val1 === $val2) return true; + + if (is_object($val1) != is_object($val2)) + { + return false; + } + if (!is_object($val1)) + { + // string ? + // todo = implement this case ? + return false; + } + + // Both values are Object sets + return $val1->HasSameContents($val2); + } } /** diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php index bbd9678c0..1a8de4b92 100644 --- a/core/bulkchange.class.inc.php +++ b/core/bulkchange.class.inc.php @@ -399,7 +399,7 @@ class BulkChange if ($sAttCode == 'id') continue; $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode); - if ($oAttDef->IsLinkSet()) + if ($oAttDef->IsLinkSet() && $oAttDef->IsIndirect()) { try { @@ -435,35 +435,38 @@ class BulkChange { $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); } - if ($this->m_bReportHtml) - { - $sCurValue = $oTargetObj->GetAsHTML($sAttCode); - $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode); - } else { - $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter); - $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter); - } - if (isset($aErrors[$sAttCode])) - { - $aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]); - } - elseif (array_key_exists($sAttCode, $aChangedFields)) - { - if ($oTargetObj->IsNew()) + if ($this->m_bReportHtml) { - $aResults[$iCol]= new CellStatus_Void($sCurValue); + $sCurValue = $oTargetObj->GetAsHTML($sAttCode); + $sOrigValue = $oTargetObj->GetOriginalAsHTML($sAttCode); } else { - $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue); + $sCurValue = $oTargetObj->GetAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter); + $sOrigValue = $oTargetObj->GetOriginalAsCSV($sAttCode, $this->m_sReportCsvSep, $this->m_sReportCsvDelimiter); + } + if (isset($aErrors[$sAttCode])) + { + $aResults[$iCol]= new CellStatus_Issue($sCurValue, $sOrigValue, $aErrors[$sAttCode]); + } + elseif (array_key_exists($sAttCode, $aChangedFields)) + { + if ($oTargetObj->IsNew()) + { + $aResults[$iCol]= new CellStatus_Void($sCurValue); + } + else + { + $aResults[$iCol]= new CellStatus_Modify($sCurValue, $sOrigValue); + } + } + else + { + // By default... nothing happens + $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); } - } - else - { - // By default... nothing happens - $aResults[$iCol]= new CellStatus_Void($aRowData[$iCol]); } } diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 62d86289d..6a40f03ab 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -826,9 +826,9 @@ abstract class DBObject } elseif(is_object($proposedValue)) { + $oLinkAttDef = MetaModel::GetAttributeDef(get_class($this), $sAtt); // The value is an object, the comparison is not strict - // #@# todo - should be even less strict => add verb on AttributeDefinition: Compare($a, $b) - if ($this->m_aOrigValues[$sAtt] != $proposedValue) + if (!$oLinkAttDef->Equals($proposedValue, $this->m_aOrigValues[$sAtt])) { $aDelta[$sAtt] = $proposedValue; } @@ -853,16 +853,58 @@ abstract class DBObject // Returns an array of attname => currentvalue public function ListChanges() { - return $this->ListChangedValues($this->m_aCurrValues); + if ($this->m_bIsInDB) + { + return $this->ListChangedValues($this->m_aCurrValues); + } + else + { + return $this->m_aCurrValues; + } } - // Tells whether or not an object was modified + // Tells whether or not an object was modified since last read (ie: does it differ from the DB ?) public function IsModified() { $aChanges = $this->ListChanges(); return (count($aChanges) != 0); } + public function Equals($oSibling) + { + if (get_class($oSibling) != get_class($this)) + { + return false; + } + if ($this->GetKey() != $oSibling->GetKey()) + { + return false; + } + if ($this->m_bIsInDB) + { + // If one has changed, then consider them as being different + if ($this->IsModified() || $oSibling->IsModified()) + { + return false; + } + } + else + { + // Todo - implement this case (loop on every attribute) + //foreach(MetaModel::ListAttributeDefs(get_class($this) as $sAttCode => $oAttDef) + //{ + //if (!isset($this->m_CurrentValues[$sAttCode])) continue; + //if (!isset($this->m_CurrentValues[$sAttCode])) continue; + //if (!$oAttDef->Equals($this->m_CurrentValues[$sAttCode], $oSibling->m_CurrentValues[$sAttCode])) + //{ + //return false; + //} + //} + return false; + } + return true; + } + // used both by insert/update private function DBWriteLinks() { diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index de3724c38..faeab6b1b 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -49,7 +49,7 @@ class DBObjectSet $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...) $this->m_aData = array(); // array of (row => array of (classalias) => object/null) - $this->m_aId2Row = array(); + $this->m_aId2Row = array(); // array of (pkey => index in m_aData) $this->m_iCurrRow = 0; } @@ -445,6 +445,43 @@ class DBObjectSet return $oNewSet; } + // Note: This verb works only with objects existing in the database + // + public function HasSameContents($oObjectSet) + { + if ($this->GetRootClass() != $oObjectSet->GetRootClass()) + { + return false; + } + if (!$this->m_bLoaded) $this->Load(); + + if ($this->Count() != $oObjectSet->Count()) + { + return false; + } + $sClassAlias = $this->m_oFilter->GetClassAlias(); + $oObjectSet->Rewind(); + while ($oObject = $oObjectSet->Fetch()) + { + $iObjectKey = $oObject->GetKey(); + if ($iObjectKey < 0) + { + return false; + } + if (!array_key_exists($iObjectKey, $this->m_aId2Row[$sClassAlias])) + { + return false; + } + $iRow = $this->m_aId2Row[$sClassAlias][$iObjectKey]; + $oSibling = $this->m_aData[$iRow][$sClassAlias]; + if (!$oObject->Equals($oSibling)) + { + return false; + } + } + return true; + } + public function CreateDelta($oObjectSet) { if ($this->GetRootClass() != $oObjectSet->GetRootClass()) diff --git a/pages/ajax.csvimport.php b/pages/ajax.csvimport.php index 5837f57fd..d9202b725 100644 --- a/pages/ajax.csvimport.php +++ b/pages/ajax.csvimport.php @@ -163,7 +163,7 @@ function GetMappingForField($sClassName, $sFieldName, $iFieldIndex, $bAdvancedMo } } } - else if ($oAttDef->IsWritable() && ($bAdvancedMode || !$oAttDef->IsLinkset())) + else if ($oAttDef->IsWritable() && (!$oAttDef->IsLinkset() || ($bAdvancedMode && $oAttDef->IsIndirect()))) { if (!$oAttDef->IsNullAllowed()) diff --git a/pages/csvimport.php b/pages/csvimport.php index 0e1eed462..6b56af49b 100644 --- a/pages/csvimport.php +++ b/pages/csvimport.php @@ -1101,24 +1101,31 @@ EOF // Create a truncated version of the data used for the fast preview // Take about 20 lines of data... knowing that some lines may contain carriage returns - $iMaxLines = 20; $iMaxLen = strlen($sUTF8Data); - $iCurPos = true; - while ( ($iCurPos > 0) && ($iMaxLines > 0)) + if ($iMaxLen > 0) { - $pos = strpos($sUTF8Data, "\n", $iCurPos); - if ($pos !== false) + $iMaxLines = 20; + $iCurPos = true; + while ( ($iCurPos > 0) && ($iMaxLines > 0)) { - $iCurPos = 1+$pos; + $pos = strpos($sUTF8Data, "\n", $iCurPos); + if ($pos !== false) + { + $iCurPos = 1+$pos; + } + else + { + $iCurPos = strlen($sUTF8Data); + $iMaxLines = 1; + } + $iMaxLines--; } - else - { - $iCurPos = strlen($sUTF8Data); - $iMaxLines = 1; - } - $iMaxLines--; + $sCSVDataTruncated = substr($sUTF8Data, 0, $iCurPos); + } + else + { + $sCSVDataTruncated = ''; } - $sCSVDataTruncated = substr($sUTF8Data, 0, $iCurPos); $sSynchroScope = utils::ReadParam('synchro_scope', ''); if (!empty($sSynchroScope)) @@ -1433,7 +1440,7 @@ EOF $oPage->output(); } -catch(CoreException $e) +catch(xxxxxxxCoreException $e) { require_once(APPROOT.'/setup/setuppage.class.inc.php'); $oP = new SetupWebPage(Dict::S('UI:PageTitle:FatalError')); @@ -1462,7 +1469,7 @@ catch(CoreException $e) // For debugging only //throw $e; } -catch(Exception $e) +catch(xxxxxException $e) { require_once(APPROOT.'/setup/setuppage.class.inc.php'); $oP = new SetupWebPage(Dict::S('UI:PageTitle:FatalError')); diff --git a/test/testlist.inc.php b/test/testlist.inc.php index fb23b84bf..e25e23495 100644 --- a/test/testlist.inc.php +++ b/test/testlist.inc.php @@ -2139,6 +2139,54 @@ class TestDataExchange extends TestBizModel protected function DoExecute() { +/* + $aScenarios = array( + array( + 'desc' => 'Load user logins', + 'login' => 'admin', + 'password' => 'admin', + 'target_class' => 'UserLocal', + 'full_load_periodicity' => 3600, // should be ignored in this case + 'reconciliation_policy' => 'use_attributes', + 'action_on_zero' => 'create', + 'action_on_one' => 'update', + 'action_on_multiple' => 'error', + 'delete_policy' => 'delete', + 'delete_policy_update' => '', + 'delete_policy_retention' => 0, + 'source_data' => array( + array('primary_key', 'login', 'password', 'profile_list'), + array( + array('user_A', 'login_A', 'password_A', 'profileid:10;reason:he/she is managing services'), + ), + ), + 'target_data' => array( + array('login'), + array( + // Initial state + ), + array( + array('login_A'), + ), + ), + 'attributes' => array( + 'login' => array( + 'do_reconcile' => true, + 'do_update' => true, + 'automatic_prefix' => true, // unique id (for unit testing) + ), + 'password' => array( + 'do_reconcile' => false, + 'do_update' => true, + ), + 'profile_list' => array( + 'do_reconcile' => false, + 'do_update' => true, + ), + ), + ), + ); +*/ $aScenarios = array( array( 'desc' => 'Simple scenario with delete option (and extkey given as org/name)', @@ -2222,11 +2270,11 @@ class TestDataExchange extends TestBizModel 'action_on_multiple' => 'error', 'delete_policy' => 'update_then_delete', 'delete_policy_update' => 'status:obsolete', - 'delete_policy_retention' => 5, + 'delete_policy_retention' => 15, 'source_data' => array( array('primary_key', 'org_id', 'name', 'status'), array( - array('obj_A', 'OMED', 'obj_A', 'production'), + array('obj_A', 'Demo', 'obj_A', 'production'), ), array( ), @@ -2248,7 +2296,7 @@ class TestDataExchange extends TestBizModel 'org_id' => array( 'do_reconcile' => true, 'do_update' => true, - 'reconciliation_attcode' => 'code', + 'reconciliation_attcode' => 'name', ), 'name' => array( 'do_reconcile' => true, diff --git a/webservices/export.php b/webservices/export.php index 0c572197e..906f20b98 100644 --- a/webservices/export.php +++ b/webservices/export.php @@ -42,7 +42,7 @@ $currentOrganization = utils::ReadParam('org_id', ''); // Main program $sExpression = utils::ReadParam('expression', ''); $sFormat = strtolower(utils::ReadParam('format', 'html')); -$sFields = utils::ReadParam('fields', ''); // CSV field list +$sFields = utils::ReadParam('fields', ''); // CSV field list (allows to specify link set attributes, still not taken into account for XML export) $oP = null;