');
class CSVParser
{
private $m_sCSVData;
private $m_sSep;
private $m_iSkip;
public function __construct($sTxt)
{
$this->m_sCSVData = $sTxt;
}
public function SetSeparator($sSep)
{
$this->m_sSep = $sSep;
}
public function GetSeparator()
{
return $this->m_sSep;
}
public function SetSkipLines($iSkip)
{
$this->m_iSkip = $iSkip;
}
public function GetSkipLines()
{
return $this->m_iSkip;
}
public function GuessSeparator()
{
// Note: skip the first line anyway
$aKnownSeps = array(';', ',', "\t"); // Use double quote for special chars!!!
$aStatsBySeparator = array();
foreach ($aKnownSeps as $sSep)
{
$aStatsBySeparator[$sSep] = array();
}
foreach(split("\n", $this->m_sCSVData) as $sLine)
{
$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)];
}
}
// Default to ','
$this->SetSeparator(",");
foreach ($aKnownSeps as $sSep)
{
// 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;
}
}
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(split("\n", $this->m_sCSVData) as $sLine)
{
$sLine = trim($sLine);
if (substr($sLine, 0, 1) == '#') continue;
if (empty($sLine)) continue;
foreach (split($this->m_sSep, $sLine) as $value)
{
if (is_numeric($value))
{
$this->SetSkipLines(0);
return 0;
}
}
$this->SetSkipLines(1);
return 1;
}
}
function ToArray($aFieldMap, $iMax = 0)
{
// $aFieldMap is an array of col_index=>col_name
// $iMax is a limit
$aRes = array();
$iCount = 0;
$iSkipped = 0;
foreach(split("\n", $this->m_sCSVData) as $sLine)
{
$sLine = trim($sLine);
if (substr($sLine, 0, 1) == '#') continue;
if (empty($sLine)) continue;
if ($iSkipped < $this->m_iSkip)
{
$iSkipped++;
continue;
}
foreach (split($this->m_sSep, $sLine) as $iCol=>$sValue)
{
if (is_array($aFieldMap)) $sColRef = $aFieldMap[$iCol];
else $sColRef = "field$iCol";
$aRes[$iCount][$sColRef] = $sValue;
}
$iCount++;
if (($iMax > 0) && ($iCount >= $iMax)) break;
}
return $aRes;
}
public function ListFields()
{
// Take the first valuable line
foreach(split("\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 (split($this->m_sSep, $sLine) as $iCol=>$value)
{
if ($this->m_iSkip == 0)
{
// No header to help us
$sLabel = "field $iCol";
}
else
{
$sLabel = "$value";
}
$aRet[] = $sLabel;
}
return $aRet;
}
}
///////////////////////////////////////////////////////////////////////////////
// External key/field naming conventions (sharing the naming space with std attributes
///////////////////////////////////////////////////////////////////////////////
function IsExtKeyField($sColDesc)
{
return ($iPos = strpos($sColDesc, EXTKEY_SEP));
}
function GetExtKeyFieldCodes($sColDesc)
{
$iPos = strpos($sColDesc, EXTKEY_SEP);
return array(
substr($sColDesc, 0, $iPos),
substr($sColDesc, $iPos + strlen(EXTKEY_SEP))
);
}
function MakeExtFieldLabel($sClass, $sExtKeyAttCode, $sForeignAttCode)
{
$oExtKeyAtt = MetaModel::GetAttributeDef($sClass, $sExtKeyAttCode);
$oForeignAtt = MetaModel::GetAttributeDef($oExtKeyAtt->GetTargetClass(), $sForeignAttCode);
return $oExtKeyAtt->GetLabel().EXTKEY_LABELSEP.$oForeignAtt->GetLabel();
}
function MakeExtFieldSelectValue($sAttCode, $sExtAttCode)
{
return $sAttCode.EXTKEY_SEP.$sExtAttCode;
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
function ShowTableForm($oPage, $oCSVParser, $sClass)
{
$aData = $oCSVParser->ToArray(null, 3);
$aColToRow = array();
foreach($aData as $aRow)
{
foreach ($aRow as $sFieldId=>$sValue)
{
$aColToRow[$sFieldId][] = $sValue;
}
}
$aFields = array();
foreach($oCSVParser->ListFields() as $iFieldIndex=>$sFieldName)
{
$sSelField = "";
$sCHECKED = ($sFieldName == "pkey" || MetaModel::IsReconcKey($sClass, $sFoundAttCode)) ? " CHECKED" : "";
$sSelField .= " ";
$aFields["field$iFieldIndex"]["label"] = $sSelField;
$aFields["field$iFieldIndex"]["value"] = $aColToRow["field$iFieldIndex"];
}
$oPage->details($aFields);
}
function PrepareObject(&$oTargetObj, $aRowData, $aAttList, $aExtKeys, &$aWarnings, &$aErrors)
{
$aResults = array();
$aWarnings = array();
$aErrors = array();
// External keys reconciliation
//
foreach($aExtKeys as $sAttCode=>$aKeyConfig)
{
$oExtKey = MetaModel::GetAttributeDef(get_class($oTargetObj), $sAttCode);
$oReconFilter = new CMDBSearchFilter($oExtKey->GetTargetClass());
foreach ($aKeyConfig as $iCol => $sForeignAttCode)
{
// The foreign attribute is one of our reconciliation key
$sFieldId = MakeExtFieldSelectValue($sAttCode, $sForeignAttCode);
$oReconFilter->AddCondition($sForeignAttCode, $aRowData[$sFieldId], '=');
$aResults["col$iCol"]= "
".$aRowData[$sFieldId]."
";
}
$oExtObjects = new CMDBObjectSet($oReconFilter);
switch($oExtObjects->Count())
{
case 0:
$aErrors[$sAttCode] = "Object not found";
$aResults[$sAttCode]= "
".$aErrors[$sAttCode]."
";
break;
case 1:
// Do change the external key attribute
$oForeignObj = $oExtObjects->Fetch();
$oTargetObj->Set($sAttCode, $oForeignObj->GetKey());
// Report it
if (array_key_exists($sAttCode, $oTargetObj->ListChanges()))
{
$aResults[$sAttCode]= "
";
}
}
// Set the object attributes
//
foreach ($aAttList as $iCol => $sAttCode)
{
$oTargetObj->Set($sAttCode, $aRowData[$sAttCode]);
}
// Reporting on fields
//
$aChangedFields = $oTargetObj->ListChanges();
foreach ($aAttList as $iCol => $sAttCode)
{
// By default... nothing happens
$sClass = "";
$sMoreInfo = "";
// Override if the attribute has changed
if (array_key_exists($sAttCode, $aChangedFields))
{
$sClass = "csvimport_ok";
}
// Override if a warning is found
if (isset($aWarnings[$sAttCode]))
{
$sClass = "csvimport_warning";
$sMoreInfo .= ", ".$aWarnings[$sAttCode];
}
// Override if an error is found
if (isset($aErrors[$sAttCode]))
{
$sClass = "csvimport_error";
$sMoreInfo = ", ".$aErrors[$sAttCode];
}
$aResults["col$iCol"]= "
".$aRowData[$sAttCode].$sMoreInfo."
";
}
// Checks
//
if (!$oTargetObj->CheckConsistency())
{
$aErrors["GLOBAL"] = "Attributes not consistent with each others";
}
return $aResults;
}
function CreateObject(&$aResult, $iRow, $sClass, $aRowData, $aAttList, $aExtKeys, CMDBChange $oChange = null)
{
$oTargetObj = MetaModel::NewObject($sClass);
$aResult[$iRow] = PrepareObject($oTargetObj, $aRowData, $aAttList, $aExtKeys, $aWarnings, $aErrors);
if (count($aErrors) > 0)
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = "Unexpected attribute value(s)";
return;
}
// Check that any external key will have a value proposed
// Could be said once for all rows !!!
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAtt)
{
if (!$oAtt->IsExternalKey()) continue;
//if (!in_array($sAttCode, $aAttList))
//{
// $aResult[$iRow]["__STATUS__"] = "Could not be created - Missing external key (".$oAtt->GetLabel().")";
// return;
//}
}
// Optionaly record the results
//
if ($oChange)
{
$newID = $oTargetObj->DBInsertTracked($oChange);
$aResult[$iRow]["__STATUS__"] = "Created: ".$oTargetObj->GetName();
}
else
{
$aResult[$iRow]["__STATUS__"] = "Create";
}
}
function UpdateObject(&$aResult, $iRow, $oTargetObj, $aRowData, $aAttList, $aExtKeys, CMDBChange $oChange = null)
{
$aResult[$iRow] = PrepareObject($oTargetObj, $aRowData, $aAttList, $aExtKeys, $aWarnings, $aErrors);
// Reporting
//
if (count($aErrors) > 0)
{
$sErrors = implode(', ', $aErrors);
$aResult[$iRow]["__STATUS__"] = "Unexpected attribute value(s)";
return;
}
$aChangedFields = $oTargetObj->ListChanges();
if (count($aChangedFields) > 0)
{
$sVerb = $oChange ? "Updated" : "Update";
$aResult[$iRow]["__STATUS__"] = "$sVerb ".count($aChangedFields)." cols";
// Optionaly record the results
//
if ($oChange)
{
$oTargetObj->DBUpdateTracked($oChange);
}
}
else
{
$aResult[$iRow]["__STATUS__"] = "No change";
}
}
function ProcessData($oPage, $sClass, $oCSVParser, $aFieldMap, $aIsReconcKey, CMDBChange $oChange = null)
{
// Note: $oChange can be null, in which case the aim is to check what would be done
// Setup field mapping: sort out between values and other specific columns
//
$iPKeyId = null;
$aReconcilKeys = array();
$aAttList = array();
$aExtKeys = array();
foreach($aFieldMap as $sFieldId=>$sColDesc)
{
$iFieldId = (int) substr($sFieldId, strlen("field"));
if ($sColDesc == "pkey")
{
// Skip !
$iPKeyId = $iFieldId;
}
elseif ($sColDesc == "__none__")
{
// Skip !
}
elseif (IsExtKeyField($sColDesc))
{
list($sExtKeyAttCode, $sExtReconcKeyAttCode) = GetExtKeyFieldCodes($sColDesc);
$aExtKeys[$sExtKeyAttCode][$iFieldId] = $sExtReconcKeyAttCode;
}
elseif (array_key_exists($sFieldId, $aIsReconcKey))
{
$aReconcilKeys[$iFieldId] = $sColDesc;
$aAttList[$iFieldId] = $sColDesc; // A reconciliation key is also a field
}
else
{
// $sColDesc is an attribute code
//
$aAttList[$iFieldId] = $sColDesc;
}
}
// Setup result presentation
//
$aDisplayConfig = array();
$aDisplayConfig["__RECONCILIATION__"] = array("label"=>"Reconciliation", "description"=>"");
$aDisplayConfig["__STATUS__"] = array("label"=>"Status", "description"=>"");
if (isset($iPKeyId))
{
$aDisplayConfig["col$iPKeyId"] = array("label"=>"pkey", "description"=>"");
}
foreach($aReconcilKeys as $iCol=>$sAttCode)
{
$sLabel = MetaModel::GetAttributeDef($sClass, $sAttCode)->GetLabel();
$aDisplayConfig["col$iCol"] = array("label"=>"$sLabel", "description"=>"");
}
foreach($aExtKeys as $sAttCode=>$aKeyConfig)
{
$oExtKeyAtt = MetaModel::GetAttributeDef($sClass, $sAttCode);
$sLabel = $oExtKeyAtt->GetLabel();
$aDisplayConfig[$sAttCode] = array("label"=>"$sLabel", "description"=>"");
foreach ($aKeyConfig as $iCol => $sForeignAttCode)
{
// The foreign attribute is one of our reconciliation key
$sLabel = MakeExtFieldLabel($sClass, $sAttCode, $sForeignAttCode);
$aDisplayConfig["col$iCol"] = array("label"=>"$sLabel", "description"=>"");
}
}
foreach ($aAttList as $iCol => $sAttCode)
{
$sLabel = MetaModel::GetAttributeDef($sClass, $sAttCode)->GetLabel();
$aDisplayConfig["col$iCol"] = array("label"=>"$sLabel", "description"=>"");
}
// Compute the results
//
$aResult = array();
foreach($oCSVParser->ToArray(array_values($aFieldMap)) as $iRow => $aRowData)
{
$oReconciliationFilter = new CMDBSearchFilter($sClass);
if (isset($iPKeyId))
{
$oReconciliationFilter->AddCondition("pkey", $aRowData["pkey"], '=');
}
foreach($aReconcilKeys as $iCol=>$sAttCode)
{
$sSearchAttCode = $aFieldMap['field'.$iCol];
$oReconciliationFilter->AddCondition($sSearchAttCode, $aRowData[$sSearchAttCode], '=');
}
$oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
switch($oReconciliationSet->Count())
{
case 0:
CreateObject($aResult, $iRow, $sClass, $aRowData, $aAttList, $aExtKeys, $oChange);
// $aResult[$iRow]["__STATUS__"]=> set in CreateObject
$aResult[$iRow]["__RECONCILIATION__"] = "Object not found";
break;
case 1:
$oTargetObj = $oReconciliationSet->Fetch();
UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $aAttList, $aExtKeys, $oChange);
$aResult[$iRow]["__RECONCILIATION__"] = "Found a match: ".$oTargetObj->GetName();
// $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
break;
default:
foreach ($aAttList as $iCol => $sAttCode)
{
$aResult[$iRow]["col$iCol"]= $aRowData[$sAttCode];
}
$aResult[$iRow]["__RECONCILIATION__"] = "Found ".$oReconciliationSet->Count()." matches";
$aResult[$iRow]["__STATUS__"]= "skipped";
}
// Whatever happened, do report the reconciliation values
if (isset($iPKeyId))
{
$aResult[$iRow]["col$iPKeyId"] = "
".$aRowData["pkey"]."
";
}
foreach($aReconcilKeys as $iCol=>$sAttCode)
{
$aResult[$iRow]["col$iCol"] = "
");
$sCSVData = utils::ReadPostedParam('csvdata');
$oPage->add("");
// As a help to the end-user, let's display the list of possible fields
// for a class, that can be copied/pasted into the CSV area.
$sCurrentList = "";
$aHeadersList = array();
foreach(MetaModel::GetClasses('bizmodel') as $sClassName)
{
$aList = MetaModel::GetZListItems($sClassName, 'details');
$aHeader = array();
// $aHeader[] = MetaModel::GetKeyLabel($sClassName);
$aHeader[] = 'pkey'; // Should be what's coded on the line above... but there is a bug
foreach($aList as $sAttCode)
{
$aHeader[] = MetaModel::GetLabel($sClassName, $sAttCode);
}
$sAttributes = implode(",", $aHeader);
$aHeadersList[$sClassName] = $sAttributes;
if($sClassName == $sClass)
{
// this class is currently selected
$sCurrentList = $sAttributes;
}
}
// Store all the values in a variable client-side
$aScript = array();
foreach($aHeadersList as $sClassName => $sAttributes)
{
$aScript[] = "'$sClassName':'$sAttributes'";
}
$oPage->add("\n");
$oPage->add_ready_script("$('#select_class').change( function() {DisplayFields(this.value);} );");
$oPage->add("Fields for this object: ");
}
function Do_Format($oPage, $sClass)
{
$oPage->p("
Bulk load from CSV data / step 2
");
$sWiztep = "2_format";
$sCSVData = utils::ReadPostedParam('csvdata');
$oCSVParser = new CSVParser($sCSVData);
$sSep = $oCSVParser->GuessSeparator();
$iSkip = $oCSVParser->GuessSkipLines();
// No data ?
$aData = $oCSVParser->ToArray(null);
$iTarget = count($aData);
if ($iTarget == 0)
{
$oPage->add("Empty data set...");
$oPage->add("");
}
// Guess the format :
$oPage->p("Guessed separator: '$sSep' (ASCII=".ord($sSep).")");
$oPage->p("Guessed # of lines to skip: $iSkip");
$oPage->p("Target: $iTarget rows");
$oPage->Add("");
}
function DoProcessOrVerify($oPage, $sClass, CMDBChange $oChange = null)
{
$sCSVData = utils::ReadPostedParam('csvdata');
$sSep = utils::ReadPostedParam('separator');
$iSkip = utils::ReadPostedParam('skiplines');
$aFieldMap = utils::ReadPostedParam('fmap');
$aIsReconcKey = utils::ReadPostedParam('iskey');
$oCSVParser = new CSVParser($sCSVData);
$oCSVParser->SetSeparator($sSep);
$oCSVParser->SetSkipLines($iSkip);
$aData = $oCSVParser->ToArray(null);
$iTarget = count($aData);
$oPage->p("