From 64e5d57ac3ba84207b70f59d6bcd12ee74598d8d Mon Sep 17 00:00:00 2001 From: Romain Quetiez Date: Fri, 13 Jan 2012 09:11:10 +0000 Subject: [PATCH] Finalized the setup (missing files) SVN:trunk[1765] --- setup/ajax.dataloader.php | 6 +- setup/compiler.class.inc.php | 702 ++++++++++++++++++++ setup/modelfactory.class.inc.php | 978 ++++++++++++++++++++++++++++ setup/modulediscovery.class.inc.php | 274 ++++++++ setup/setuppage.class.inc.php | 2 +- 5 files changed, 1958 insertions(+), 4 deletions(-) create mode 100644 setup/compiler.class.inc.php create mode 100644 setup/modelfactory.class.inc.php create mode 100644 setup/modulediscovery.class.inc.php diff --git a/setup/ajax.dataloader.php b/setup/ajax.dataloader.php index 5e17547cd..87a35ac8f 100644 --- a/setup/ajax.dataloader.php +++ b/setup/ajax.dataloader.php @@ -140,9 +140,9 @@ try // SetupPage::log_info("Compiling data model."); - require_once(APPROOT.'designer/modulediscovery.class.inc.php'); - require_once(APPROOT.'designer/modelfactory.inc.class.php'); - require_once(APPROOT.'designer/compiler.inc.class.php'); + require_once(APPROOT.'setup/modulediscovery.class.inc.php'); + require_once(APPROOT.'setup/modelfactory.class.inc.php'); + require_once(APPROOT.'setup/compiler.class.inc.php'); $sSelectedModules = Utils::ReadParam('selected_modules', '', false, 'raw_data'); $aSelectedModules = explode(',', $sSelectedModules); diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php new file mode 100644 index 000000000..715ed631d --- /dev/null +++ b/setup/compiler.class.inc.php @@ -0,0 +1,702 @@ +\n$s

\n"; + } + + public function add($s) + { + //echo $s; + } + + public function output() + { + } +} + + +/** + * Compiler class + */ +class MFCompiler +{ + protected $oFactory; + protected $sSourceDir; + + public function __construct($oModelFactory, $sSourceDir) + { + $this->oFactory = $oModelFactory; + $this->sSourceDir = $sSourceDir; + } + + public function Compile($sTargetDir, $oP = null) + { + if (is_null($oP)) + { + $oP = new CompilerEchoPage(); + } + + $aResultFiles = array(); + + $aModules = $this->oFactory->GetLoadedModules(); + foreach($aModules as $foo => $oModule) + { + $sModuleName = $oModule->GetName(); + $sModuleVersion = $oModule->GetVersion(); + + $sModuleRootDir = realpath($oModule->GetRootDir()); + $sRelativeDir = substr($sModuleRootDir, strlen($this->sSourceDir)); + + // Push the other module files + $this->CopyDirectory($sModuleRootDir, $sTargetDir.$sRelativeDir); + + $oClasses = $this->oFactory->ListClasses($sModuleName); + $iClassCount = $oClasses->length; + if ($iClassCount == 0) + { + $oP->p("Found module without classes declared: $sModuleName"); + } + else + { + $sResultFile = $sTargetDir.$sRelativeDir.'/model.'.$sModuleName.'.php'; + if (is_file($sResultFile)) + { + $oP->p("Updating $sResultFile for module $sModuleName in version $sModuleVersion ($iClassCount classes)"); + } + else + { + $sResultDir = dirname($sResultFile); + if (!is_dir($sResultDir)) + { + $oP->p("Creating directory $sResultDir"); + mkdir($sResultDir, 0777, true); + } + $oP->p("Creating $sResultFile for module $sModuleName in version $sModuleVersion ($iClassCount classes)"); + } + + // Compile the module into a single file + // + $sId = $sModuleName; + $aResultFiles[$sId] = $sResultFile; + $sCurrDate = date(DATE_ISO8601); + $sAuthor = 'Combodo compiler'; + $sLicence = 'http://www.opensource.org/licenses/gpl-3.0.html LGPL'; + $sFileHeader = +<<getAttribute("name"); + try + { + $this->CompileClass($oClass, $sResultFile, $oP); + } + catch (ssDOMFormatException $e) + { + $sClass = $oClass->getAttribute("name"); + throw new Exception("Failed to process class '$sClass', from '$sModuleRootDir': ".$e->getMessage()); + } + } + } + } + + if (count($aResultFiles)) + { + $oP->add('

Files

'); + foreach ($aResultFiles as $sModuleName => $sFile) + { + $oP->add('

'.$sFile.'

'); + $oP->add('
'); + $oP->add(highlight_file($sFile, true)); + $oP->add('
'); + } + } + + $oP->output(); + } + + /** + * Helper to copy the module files to the exploitation environment + * Returns true if successfull + */ + protected function CopyDirectory($sSource, $sDest) + { + if (is_dir($sSource)) + { + if (!is_dir($sDest)) + { + mkdir($sDest); + } + $aFiles = scandir($sSource); + if(sizeof($aFiles) > 0 ) + { + foreach($aFiles as $sFile) + { + if ($sFile == '.' || $sFile == '..' || $sFile == '.svn') + { + // Skip + continue; + } + + if (is_dir($sSource.'/'.$sFile)) + { + $this->CopyDirectory($sSource.'/'.$sFile, $sDest.'/'.$sFile); + } + else + { + copy($sSource.'/'.$sFile, $sDest.'/'.$sFile); + } + } + } + return true; + } + elseif (is_file($sSource)) + { + return copy($sSource, $sDest); + } + else + { + return false; + } + } + + + /** + * Helper to browse the DOM -could be factorized in ModelFactory + * Returns the node directly under the given node, and that is supposed to be always present and unique + */ + protected function GetUniqueElement($oDOMNode, $sTagName, $bMustExist = true) + { + $oNode = null; + foreach($oDOMNode->childNodes as $oChildNode) + { + if ($oChildNode->nodeName == $sTagName) + { + $oNode = $oChildNode; + break; + } + } + if ($bMustExist && is_null($oNode)) + { + throw new DOMFormatException('Missing unique tag: '.$sTagName); + } + return $oNode; + } + + /** + * Helper to browse the DOM -could be factorized in ModelFactory + * Returns the node directly under the given node, or null is missing + */ + protected function GetOptionalElement($oDOMNode, $sTagName) + { + return $this->GetUniqueElement($oDOMNode, $sTagName, false); + } + + + /** + * Helper to browse the DOM -could be factorized in ModelFactory + * Returns the TEXT of the given node (possibly from several subnodes) + */ + protected function GetNodeText($oNode) + { + $sText = ''; + foreach($oNode->childNodes as $oChildNode) + { + if ($oChildNode instanceof DOMCharacterData) // Base class of DOMText and DOMCdataSection + { + $sText .= $oChildNode->wholeText; + } + } + return $sText; + } + + /** + * Helper to browse the DOM -could be factorized in ModelFactory + * Assumes the given node to be either a text or + * + * value + * value + * + * where value can be the either a text or an array of items... recursively + * Returns a PHP array + */ + protected function GetNodeAsArrayOfItems($oNode) + { + $oItems = $this->GetOptionalElement($oNode, 'items'); + if ($oItems) + { + $res = array(); + foreach($oItems->childNodes as $oItem) + { + // When an attribute is msising + if ($oItem->hasAttribute('key')) + { + $key = $oItem->getAttribute('key'); + $res[$key] = $this->GetNodeAsArrayOfItems($oItem); + } + else + { + $res[] = $this->GetNodeAsArrayOfItems($oItem); + } + } + } + else + { + $res = $this->GetNodeText($oNode); + } + return $res; + } + + + + /** + * Helper to format the flags for an attribute, in a given state + * @param object $oAttNode DOM node containing the information to build the flags + * Returns string PHP flags, based on the OPT_ATT_ constants, or empty (meaning 0, can be omitted) + */ + protected function FlagsToPHP($oAttNode) + { + static $aNodeAttributeToFlag = array( + 'read_only' => 'OPT_ATT_READONLY', + 'must_prompt' => 'OPT_ATT_MUSTPROMPT', + 'must_change' => 'OPT_ATT_MUSTCHANGE', + 'hidden' => 'OPT_ATT_HIDDEN', + ); + + $aFlags = array(); + foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag) + { + $bFlag = ($oAttNode->GetAttribute($sNodeAttribute) == '1'); + if ($bFlag) + { + $aFlags[] = $sFlag; + } + } + $sRes = implode(' | ', $aFlags); + return $sRes; + } + + + protected function CompileClass($oClass, $sResFile, $oP) + { + $sClass = $oClass->getAttribute('name'); + $oProperties = $this->GetUniqueElement($oClass, 'properties'); + + // Class caracteristics + // + $aClassParams = array(); + $aClassParams['category'] = "'".$oClass->getAttribute('category')."'"; + $aClassParams['key_type'] = "'autoincrement'"; + + $oNaming = $this->GetUniqueElement($oProperties, 'naming'); + $oNameAttributes = $this->GetUniqueElement($oNaming, 'attributes'); + $oAttributes = $oNameAttributes->getElementsByTagName('attribute'); + $aNameAttCodes = array(); + foreach($oAttributes as $oAttribute) + { + $aNameAttCodes[] = $oAttribute->getAttribute('name'); + } + if (count($aNameAttCodes) > 1) + { + // New style... + $sNameAttCode = "array('".implode("', '", $aNameAttCodes)."')"; + } + elseif (count($aNameAttCodes) == 1) + { + // New style... + $sNameAttCode = "'$aNameAttCodes[0]'"; + } + else + { + $sNameAttCode = "''"; + } + $aClassParams['name_attcode'] = $sNameAttCode; + + $oLifecycle = $this->GetOptionalElement($oClass, 'lifecycle'); + if ($oLifecycle) + { + $sStateAttCode = $oLifecycle->getAttribute('attribute'); + } + else + { + $sStateAttCode = ""; + } + $aClassParams['state_attcode'] = "'$sStateAttCode'"; + + $oReconciliation = $this->GetUniqueElement($oProperties, 'reconciliation'); + $oReconcAttributes = $oReconciliation->getElementsByTagName('attribute'); + $aReconcAttCodes = array(); + foreach($oReconcAttributes as $oAttribute) + { + $aReconcAttCodes[] = $oAttribute->getAttribute('name'); + } + $sReconcKeys = "array('".implode("', '", $aReconcAttCodes)."')"; + $aClassParams['reconc_keys'] = $sReconcKeys; + + $aClassParams['db_table'] = "'".$oClass->getAttribute('db_table')."'"; + $aClassParams['db_key_field'] = "'".$oClass->getAttribute('db_key_field')."'"; + $aClassParams['db_finalclass_field'] = "'".$oClass->getAttribute('db_final_class_field')."'"; + + $oDisplayTemplate = $this->GetOptionalElement($oProperties, 'display_template'); + if ($oDisplayTemplate) + { + $sDisplayTemplate = $oDisplayTemplate->textContent; + $aClassParams['display_template'] = "'$sDisplayTemplate'"; + } + + $oIcon = $this->GetOptionalElement($oProperties, 'icon'); + if ($oIcon) + { + $sIcon = $oIcon->textContent; + $aClassParams['icon'] = "'$sIcon'"; + } + + // Finalize class params declaration + // + $aClassParamsPHP = array(); + foreach($aClassParams as $sKey => $sPHPValue) + { + $aClassParamsPHP[] = " '$sKey' => $sPHPValue,"; + } + $sClassParams = implode("\n", $aClassParamsPHP); + + // Comment on top of the class declaration + // + $oComment = $this->GetOptionalElement($oProperties, 'comment'); + if ($oComment) + { + $sCodeComment = $oComment->textContent; + } + else + { + $sCodeComment = ''; + } + + // Fields + // + $sAttributes = ''; + foreach($this->oFactory->ListFields($oClass) as $oField) + { + // $oField + $sAttCode = $oField->getAttribute('name'); + $sAttType = 'Attribute'.$oField->getAttribute('type'); + + $aDependencies = array(); + $oDependencies = $this->GetOptionalElement($oField, 'dependencies'); + if (!is_null($oDependencies)) + { + $oDepNodes = $oDependencies->getElementsByTagName('attribute'); + foreach($oDepNodes as $oDepAttribute) + { + $aDependencies[] = "'".$oDepAttribute->getAttribute('name')."'"; + } + } + $sDependencies = 'array('.implode(', ', $aDependencies).')'; + + $aParameters = array(); + + if ($sAttType == 'AttributeLinkedSetIndirect') + { + $aParameters['linked_class'] = "'".$oField->getAttribute('linked_class')."'"; + $aParameters['ext_key_to_me'] = "'".$oField->getAttribute('ext_key_to_me')."'"; + $aParameters['ext_key_to_remote'] = "'".$oField->getAttribute('ext_key_to_remote')."'"; + // todo - utile ? + $aParameters['allowed_values'] = 'null'; + $aParameters['count_min'] = $oField->getAttribute('count_min'); + $aParameters['count_max'] = $oField->getAttribute('count_max'); + $aParameters['depends_on'] = $sDependencies; + } + elseif ($sAttType == 'AttributeLinkedSet') + { + $aParameters['linked_class'] = "'".$oField->getAttribute('linked_class')."'"; + $aParameters['ext_key_to_me'] = "'".$oField->getAttribute('ext_key_to_me')."'"; + // todo - utile ? + $aParameters['allowed_values'] = 'null'; + $aParameters['count_min'] = $oField->getAttribute('count_min'); + $aParameters['count_max'] = $oField->getAttribute('count_max'); + $aParameters['depends_on'] = $sDependencies; + } + elseif ($sAttType == 'AttributeExternalKey') + { + $aParameters['targetclass'] = "'".$oField->getAttribute('target_class')."'"; + // todo = v�rifier l'utilit� + $aParameters['jointype'] = 'null'; + if (($sOql = $oField->getAttribute('filter')) != '') + { + $sEscapedOql = addslashes($sOql); + $aParameters['allowed_values'] = "new ValueSetObjects('$sEscapedOql')"; // or "new ValueSetObjects('SELECT xxxx')" + } + else + { + $aParameters['allowed_values'] = 'null'; // or "new ValueSetObjects('SELECT xxxx')" + } + $aParameters['sql'] = "'".$oField->getAttribute('sql')."'"; + $aParameters['is_null_allowed'] = $oField->getAttribute('is_null_allowed') == 'true' ? 'true' : 'false'; + $aParameters['on_target_delete'] = $oField->getAttribute('on_target_delete'); + $aParameters['depends_on'] = $sDependencies; + } + elseif ($sAttType == 'AttributeHierarchicalKey') + { + if (($sOql = $oField->getAttribute('filter')) != '') + { + $sEscapedOql = addslashes($sOql); + $aParameters['allowed_values'] = "new ValueSetObjects('$sEscapedOql')"; // or "new ValueSetObjects('SELECT xxxx')" + } + else + { + $aParameters['allowed_values'] = 'null'; // or "new ValueSetObjects('SELECT xxxx')" + } + $aParameters['sql'] = "'".$oField->getAttribute('sql')."'"; + $aParameters['is_null_allowed'] = $oField->getAttribute('is_null_allowed') == 'true' ? 'true' : 'false'; + $aParameters['on_target_delete'] = $oField->getAttribute('on_target_delete'); + $aParameters['depends_on'] = $sDependencies; + } + elseif ($sAttType == 'AttributeExternalField') + { + $aParameters['allowed_values'] = 'null'; + $aParameters['extkey_attcode'] = "'".$oField->getAttribute('extkey_attcode')."'"; + $aParameters['target_attcode'] = "'".$oField->getAttribute('target_attcode')."'"; + } + elseif ($sAttType == 'AttributeURL') + { + $aParameters['target'] = "'".$oField->getAttribute('target')."'"; + $aParameters['allowed_values'] = 'null'; + $aParameters['sql'] = "'".$oField->getAttribute('sql')."'"; + $aParameters['default_value'] = "'".$oField->getAttribute('default_value')."'"; + $aParameters['is_null_allowed'] = $oField->getAttribute('is_null_allowed') == 'true' ? 'true' : 'false'; + $aParameters['depends_on'] = $sDependencies; + } + elseif ($sAttType == 'AttributeEnum') + { + $oValues = $this->GetUniqueElement($oField, 'values'); + $oValueNodes = $oValues->getElementsByTagName('value'); + $aValues = array(); + foreach($oValueNodes as $oValue) + { + // new style... $aValues[] = "'".addslashes($oValue->textContent)."'"; + $aValues[] = $oValue->textContent; + } + // new style... $sValues = 'array('.implode(', ', $aValues).')'; + $sValues = '"'.implode(',', $aValues).'"'; + $aParameters['allowed_values'] = "new ValueSetEnum($sValues)"; + $aParameters['sql'] = "'".$oField->getAttribute('sql')."'"; + $aParameters['default_value'] = "'".$oField->getAttribute('default_value')."'"; + $aParameters['is_null_allowed'] = $oField->getAttribute('is_null_allowed') == 'true' ? 'true' : 'false'; + $aParameters['depends_on'] = $sDependencies; + } + elseif ($sAttType == 'AttributeBlob') + { + $aParameters['depends_on'] = $sDependencies; + } + else + { + $aParameters['allowed_values'] = 'null'; // or "new ValueSetEnum('SELECT xxxx')" + $aParameters['sql'] = "'".$oField->getAttribute('sql')."'"; + $aParameters['default_value'] = "'".$oField->getAttribute('default_value')."'"; + $aParameters['is_null_allowed'] = $oField->getAttribute('is_null_allowed') == 'true' ? 'true' : 'false'; + $aParameters['depends_on'] = $sDependencies; + + if ($sValidationPattern = $oField->getAttribute('validation_pattern')) + { + $aParameters['validation_pattern'] = '"'.addslashes($sValidationPattern).'"'; + } + } + + $aParams = array(); + foreach($aParameters as $sKey => $sValue) + { + $aParams[] = '"'.$sKey.'"=>'.$sValue; + } + $sParams = implode(', ', $aParams); + $sAttributes .= " MetaModel::Init_AddAttribute(new $sAttType(\"$sAttCode\", array($sParams)));\n"; + } + + // Lifecycle + // + $sLifecycle = ''; + if ($oLifecycle) + { + $sLifecycle .= "\t\t// Lifecycle (status attribute: $sStateAttCode)\n"; + $sLifecycle .= "\t\t//\n"; + + $oStimuli = $this->GetUniqueElement($oLifecycle, 'stimuli'); + foreach ($oStimuli->getElementsByTagName('stimulus') as $oStimulus) + { + $sStimulus = $oStimulus->getAttribute('name'); + $sStimulusClass = $oStimulus->getAttribute('type'); + + $sLifecycle .= " MetaModel::Init_DefineStimulus(new ".$sStimulusClass."(\"".$sStimulus."\", array()));\n"; + } + + $oStates = $this->GetUniqueElement($oLifecycle, 'states'); + foreach ($oStates->getElementsByTagName('state') as $oState) + { + $sState = $oState->getAttribute('name'); + + $sLifecycle .= " MetaModel::Init_DefineState(\n"; + $sLifecycle .= " \"".$sState."\",\n"; + $sLifecycle .= " array(\n"; + $sLifecycle .= " \"attribute_inherit\" => '',\n"; + $sLifecycle .= " \"attribute_list\" => array(\n"; + + $oFlags = $this->GetUniqueElement($oState, 'flags'); + foreach ($oFlags->getElementsByTagName('attribute') as $oAttributeNode) + { + $sFlags = $this->FlagsToPHP($oAttributeNode); + if (strlen($sFlags) > 0) + { + $sAttCode = $oAttributeNode->GetAttribute('name'); + $sLifecycle .= " '$sAttCode' => $sFlags,\n"; + } + } + + $sLifecycle .= " ),\n"; + $sLifecycle .= " )\n"; + $sLifecycle .= " );\n"; + + $oTransitions = $this->GetUniqueElement($oState, 'transitions'); + foreach ($oTransitions->getElementsByTagName('transition') as $oTransition) + { + $sStimulus = $oTransition->getAttribute('stimulus'); + $sTargetState = $oTransition->getAttribute('target'); + + $oActions = $this->GetUniqueElement($oTransition, 'actions'); + $aVerbs = array(); + foreach ($oActions->getElementsByTagName('action') as $oAction) + { + $sVerb = $oAction->getAttribute('verb'); + $aVerbs[] = "'$sVerb'"; + } + $sActions = implode(', ', $aVerbs); + $sLifecycle .= " MetaModel::Init_DefineTransition(\"$sState\", \"$sStimulus\", array(\"target_state\"=>\"$sTargetState\", \"actions\"=>array($sActions), \"user_restriction\"=>null));\n"; + } + } + } + + // ZLists + // + $aListRef = array( + 'details' => 'details', + 'standard_search' => 'search', + 'list' => 'list' + ); + + $oPresentation = $this->GetUniqueElement($oClass, 'presentation'); + $sZlists = ''; + foreach ($aListRef as $sListCode => $sListTag) + { + $oListNode = $this->GetUniqueElement($oPresentation, $sListTag); + $aAttributes = $this->GetNodeAsArrayOfItems($oListNode); + + $sZAttributes = var_export($aAttributes, true); + $sZlists .= " MetaModel::Init_SetZListItems('$sListCode', $sZAttributes);\n"; + } + + // Methods + $sMethods = ""; + $oMethods = $this->GetUniqueElement($oClass, 'methods'); + foreach($oMethods->getElementsByTagName('method') as $oMethod) + { + $sMethodCode = $this->GetNodeText($oMethod); + $oMethodComment = $this->GetOptionalElement($oMethod, 'comment'); + if ($oMethodComment) + { + $sMethods .= "\n\t".$oMethodComment->textContent."\n".$sMethodCode."\n"; + } + else + { + $sMethods .= "\n\n".$sMethodCode."\n"; + } + } + + // Let's make the whole class declaration + // + $sPHP = "\n\n$sCodeComment\n"; + if ($oClass->getAttribute('abstract') == 'true') + { + $sPHP .= 'abstract class '.$oClass->getAttribute('name'); + } + else + { + $sPHP .= 'class '.$oClass->getAttribute('name'); + } + $sPHP .= " extends ".$oClass->getAttribute('parent')."\n"; + $sPHP .= +<< diff --git a/setup/modelfactory.class.inc.php b/setup/modelfactory.class.inc.php new file mode 100644 index 000000000..5e564de04 --- /dev/null +++ b/setup/modelfactory.class.inc.php @@ -0,0 +1,978 @@ + + * @author Romain Quetiez + * @author Denis Flaven + * @license Combodo Private + */ + + +require_once(APPROOT.'setup/moduleinstaller.class.inc.php'); + + + /** + * ModelFactoryItem: an item managed by the ModuleFactory + * @package ModelFactory + */ +abstract class MFItem +{ + public function __construct($sName, $sValue) + { + parent::__construct($sName, $sValue); + } + + /** + * List the source files for this item + */ + public function ListSources() + { + + } + /** + * List the rights/restrictions for this item + */ + public function ListRights() + { + + } +} + /** + * ModelFactoryModule: the representation of a Module (i.e. element that can be selected during the setup) + * @package ModelFactory + */ +class MFModule extends MFItem +{ + protected $sId; + protected $sName; + protected $sVersion; + protected $sRootDir; + protected $sLabel; + protected $aDataModels; + + public function __construct($sId, $sRootDir, $sLabel) + { + $this->sId = $sId; + + list($this->sName, $this->sVersion) = ModuleDiscovery::GetModuleName($sId); + if (strlen($this->sVersion) == 0) + { + $this->sVersion = '1.0.0'; + } + + $this->sRootDir = $sRootDir; + $this->sLabel = $sLabel; + $this->aDataModels = array(); + + // Scan the module's root directory to find the datamodel(*).xml files + if ($hDir = opendir($sRootDir)) + { + // This is the correct way to loop over the directory. (according to the documentation) + while (($sFile = readdir($hDir)) !== false) + { + if (preg_match('/^datamodel(.*)\.xml$/i', $sFile, $aMatches)) + { + $this->aDataModels[] = $this->sRootDir.'/'.$aMatches[0]; + } + } + closedir($hDir); + } + } + + + public function GetId() + { + return $this->sId; + } + + public function GetName() + { + return $this->sName; + } + + public function GetVersion() + { + return $this->sVersion; + } + + public function GetLabel() + { + return $this->sLabel; + } + + public function GetRootDir() + { + return $this->sRootDir; + } + + public function GetDataModelFiles() + { + return $this->aDataModels; + } + + /** + * List all classes in this module + */ + public function ListClasses() + { + return array(); + } +} + +class MFWorkspace extends MFModule +{ + public function __construct($sRootDir) + { + $this->sId = 'itop-workspace'; + + $this->sName = 'workspace'; + $this->sVersion = '1.0'; + + $this->sRootDir = $sRootDir; + $this->sLabel = 'Workspace'; + $this->aDataModels = array(); + + $this->aDataModels[] = $this->GetWorkspacePath(); + } + + public function GetWorkspacePath() + { + return $this->sRootDir.'/workspace.xml'; + } +} + + /** + * ModelFactoryClass: the representation of a Class (i.e. a PHP class) + * @package ModelFactory + */ +class MFClass extends MFItem +{ + /** + * List all fields of this class + */ + public function ListFields() + { + return array(); + } + + /** + * List all methods of this class + */ + public function ListMethods() + { + return array(); + } + + /** + * Whether or not the class has a lifecycle + * @return bool + */ + public function HasLifeCycle() + { + return true; //TODO implement + } + + /** + * Returns the code of the attribute used to store the lifecycle state + * @return string + */ + public function GetLifeCycleAttCode() + { + if ($this->HasLifeCycle()) + { + + } + return ''; + } + + /** + * List all states of this class + */ + public function ListStates() + { + return array(); + } + /** + * List all relations of this class + */ + public function ListRelations() + { + return array(); + } + /** + * List all transitions of this class + */ + public function ListTransitions() + { + return array(); + } +} + + /** + * ModelFactoryField: the representation of a Field (i.e. a property of a class) + * @package ModelFactory + */ +class MFField extends MFItem +{ +} + + /** + * ModelFactoryMethod: the representation of a Method (i.e. a method of a class) + * @package ModelFactory + */ +class MFMethod extends MFItem +{ +} + + /** + * ModelFactoryState: the representation of a state in the life cycle of the class + * @package ModelFactory + */ +class MFState extends MFItem +{ +} + + /** + * ModelFactoryRelation: the representation of a n:n relationship between two classes + * @package ModelFactory + */ +class MFRelation extends MFItem +{ +} + + /** + * ModelFactoryTransition: the representation of a transition between two states in the life cycle of the class + * @package ModelFactory + */ +class MFTransition extends MFItem +{ +} + + +/** + * ModelFactory: the class that manages the in-memory representation of the XML MetaModel + * @package ModelFactory + */ +class ModelFactory +{ + protected $sRootDir; + protected $oDOMDocument; + protected $oClasses; + static protected $aLoadedClasses; + static protected $aWellKnownParents = array('DBObject', 'CMDBObject','cmdbAbstractObject'); + static protected $aLoadedModules; + + + public function __construct($sRootDir) + { + $this->sRootDir = $sRootDir; + $this->oDOMDocument = new DOMDocument('1.0', 'UTF-8'); + $this->oClasses = $this->oDOMDocument->CreateElement('classes'); + $this->oDOMDocument->AppendChild($this->oClasses); + self::$aLoadedClasses = array(); + self::$aLoadedModules = array(); + } + + public function Dump($oNode = null) + { + if (is_null($oNode)) + { + $oNode = $this->oClasses; + } + echo htmlentities($this->oDOMDocument->saveXML($oNode)); + } + /** + * Loads the definitions corresponding to the given Module + * @param MFModule $oModule + */ + public function LoadModule(MFModule $oModule) + { + $aDataModels = $oModule->GetDataModelFiles(); + $sModuleName = $oModule->GetName(); + $aClasses = array(); + self::$aLoadedModules[] = $oModule; + foreach($aDataModels as $sXmlFile) + { + $oDocument = new DOMDocument('1.0', 'UTF-8'); + $oDocument->load($sXmlFile); + $oXPath = new DOMXPath($oDocument); + $oNodeList = $oXPath->query('//*'); + foreach($oNodeList as $oNode) + { + $oNode->SetAttribute('_source', $sXmlFile); + } + $oXPath = new DOMXPath($oDocument); + $oNodeList = $oXPath->query('//classes/class'); + foreach($oNodeList as $oNode) + { + if ($oNode->hasAttribute('parent')) + { + $sParentClass = $oNode->GetAttribute('parent'); + } + else + { + $sParentClass = ''; + } + $sClassName = $oNode->GetAttribute('name'); + $aClasses[$sClassName] = array('name' => $sClassName, 'parent' => $sParentClass, 'node' => $oNode); + } + } + + $index = 1; + do + { + $bNothingLoaded = true; + foreach($aClasses as $sClassName => $aClassData) + { + $sOperation = $aClassData['node']->getAttribute('_operation'); + switch($sOperation) + { + case 'created': + case '': + if (in_array($aClassData['parent'], self::$aWellKnownParents)) + { + $this->AddClass($aClassData['node'], $sModuleName); + unset($aClasses[$sClassName]); + $bNothingLoaded = false; + } + else if ($this->ClassNameExists($aClassData['parent'])) + { + $this->AddClass($aClassData['node'], $sModuleName); + unset($aClasses[$sClassName]); + $bNothingLoaded = false; + } + break; + + case 'removed': + unset($aClasses[$sClassName]); + $this->RemoveClass($sClassName); + break; + + case 'modified': + unset($aClasses[$sClassName]); + $this->AlterClass($aClassData['node'], $sModuleName); + break; + } + } + $index++; + } + while((count($aClasses)>0) && !$bNothingLoaded); + + // The remaining classes have no known parent, let's add them at the root + foreach($aClasses as $sClassName => $aClassData) + { + $sOperation = $aClassData['node']->getAttribute('_operation'); + switch($sOperation) + { + case 'created': + case '': + // Add the class as a new root class + $this->AddClass($aClassData['node'], $sModuleName); + $oNewNode = $this->oDOMDocument->ImportNode($aClassData['node']); + break; + + case 'removed': + $this->RemoveClass($sClassName); + break; + + case 'modified': + //@@TODO Handle the modification of a class here + $this->AlterClass($aClassData['node'], $sModuleName); + break; + } + } + } + + function GetLoadedModules($bExcludeWorkspace = true) + { + if ($bExcludeWorkspace) + { + $aModules = array(); + foreach(self::$aLoadedModules as $oModule) + { + if (!$oModule instanceof MFWorkspace) + { + $aModules[] = $oModule; + } + } + } + else + { + $aModules = self::$aLoadedModules; + } + return $aModules; + } + + /** + * Check if the class specified by the given node already exists in the loaded DOM + * @param DOMNode $oClassNode The node corresponding to the class to load + * @throws Exception + * @return bool True if the class exists, false otherwise + */ + protected function ClassExists(DOMNode $oClassNode) + { + if ($oClassNode->hasAttribute('name')) + { + $sClassName = $oClassNode->GetAttribute('name'); + } + else + { + throw new Exception('ModelFactory::AddClass: Cannot add a class with no name'); + } + + return (array_key_exists($sClassName, self::$aLoadedClasses)); + } + + /** + * Check if the class specified by the given name already exists in the loaded DOM + * @param string $sClassName The node corresponding to the class to load + * @throws Exception + * @return bool True if the class exists, false otherwise + */ + protected function ClassNameExists($sClassName) + { + return (array_key_exists($sClassName, self::$aLoadedClasses)); + } + /** + * Add the given class to the DOM + * @param DOMNode $oClassNode + * @param string $sModuleName The name of the module in which this class is declared + * @throws Exception + */ + protected function AddClass(DOMNode $oClassNode, $sModuleName) + { + if ($oClassNode->hasAttribute('name')) + { + $sClassName = $oClassNode->GetAttribute('name'); + } + else + { + throw new Exception('ModelFactory::AddClass: Cannot add a class with no name'); + } + if ($this->ClassExists($oClassNode)) + { + throw new Exception("ModelFactory::AddClass: Cannot add the already existing class $sClassName"); + } + + $sParentClass = ''; + if ($oClassNode->hasAttribute('parent')) + { + $sParentClass = $oClassNode->GetAttribute('parent'); + } + + //echo "Adding class: $sClassName, parent: $sParentClass
"; + if (!in_array($sParentClass, self::$aWellKnownParents) && $this->ClassNameExists($sParentClass)) + { + // The class is a subclass of a class already loaded, add it under + self::$aLoadedClasses[$sClassName] = $this->oDOMDocument->ImportNode($oClassNode, true /* bDeep */); + self::$aLoadedClasses[$sClassName]->SetAttribute('_operation', 'added'); + self::$aLoadedClasses[$sClassName]->SetAttribute('_created_in', $sModuleName); + self::$aLoadedClasses[$sParentClass]->AppendChild(self::$aLoadedClasses[$sClassName]); + $bNothingLoaded = false; + } + else if (in_array($sParentClass, self::$aWellKnownParents)) + { + // Add the class as a new root class + self::$aLoadedClasses[$sClassName] = $this->oDOMDocument->ImportNode($oClassNode, true /* bDeep */); + self::$aLoadedClasses[$sClassName]->SetAttribute('_operation', 'added'); + self::$aLoadedClasses[$sClassName]->SetAttribute('_created_in', $sModuleName); + $this->oClasses->AppendChild(self::$aLoadedClasses[$sClassName]); + } + else + { + throw new Exception("ModelFactory::AddClass: Cannot add the class $sClassName, unknown parent class: $sParentClass"); + } + } + + /** + * Remove a class from the DOM + * @param string $sClass + * @throws Exception + */ + public function RemoveClass($sClass) + { + if (!$this->ClassNameExists($sClass)) + { + throw new Exception("ModelFactory::RemoveClass: Cannot remove the non existing class $sClass"); + } + self::$aLoadedClasses[$sClass]->SetAttribute('_operation', 'removed'); + + //TODO: also mark as removed the child classes + } + + public function AlterClass(DOMNode $oClassNode, $sModuleName) + { + if ($oClassNode->hasAttribute('name')) + { + $sClassName = $oClassNode->GetAttribute('name'); + } + else + { + throw new Exception('ModelFactory::AddClass: Cannot alter a class with no name'); + } + if (!$this->ClassExists($oClassNode)) + { + throw new Exception("ModelFactory::AddClass: Cannot later the non-existing class $sClassName"); + } + $this->_priv_AlterNode(self::$aLoadedClasses[$sClassName], $oClassNode); + } + + protected function _priv_AlterNode(DOMNode $oNode, DOMNode $oDeltaNode) + { + foreach($oDeltaNode->childNodes as $oChildNode) + { + $sOperation = $oChildNode->getAttribute('_operation'); + $sPath = $oChildNode->tagName; + $sName = $oChildNode->getAttribute('name'); + if ($sName != '') + { + $sPath .= "[@name='$sName']"; + } + switch($sOperation) + { + case 'removed': + $oToRemove = $this->_priv_GetNodes($sPath, $oNode)->item(0); + if ($oToRemove != null) + { + $this->_priv_SetFlag($oToRemove, 'removed'); + } + break; + + case 'modified': + $oToModify = $this->_priv_GetNodes($sPath, $oNode)->item(0); + if ($oToModify != null) + { + $this->_priv_AlterNode($oToModify, $oChildNode); + } + else + { + throw new Exception("Cannot modify the non-existing node '$sPath' in '".$oNode->getNodePath()."'"); + } + break; + + case 'created': + $oNewNode = $this->oDOMDocument->importNode($oChildNode); + $oNode->appendChild($oNewNode); + $this->_priv_SetFlag($oNewNode, 'created'); + break; + + case '': + // Do nothing + } + } + } + /** + * List all classes from the DOM, for a given module + * @param string $sModuleNale + * @param bool $bFlattenLayers + * @throws Exception + */ + public function ListClasses($sModuleName, $bFlattenLayers = true) + { + $sXPath = "//class[@_created_in='$sModuleName']"; + if ($bFlattenLayers) + { + $sXPath = "//class[@_created_in='$sModuleName' and @_operation!='removed']"; + } + return $this->_priv_GetNodes($sXPath); + } + + public function GetClass($sClassName, $bFlattenLayers = true) + { + if (!$this->ClassNameExists($sClassName)) + { + return null; + } + $oClassNode = self::$aLoadedClasses[$sClassName]; + if ($bFlattenLayers) + { + $sOperation = $oClassNode->getAttribute('_operation'); + if ($sOperation == 'removed') + { + $oClassNode = null; + } + } + return $oClassNode; + } + + /** + * List all classes from the DOM + * @param bool $bFlattenLayers + * @throws Exception + */ + public function ListFields(DOMNode $oClassNode, $bFlattenLayers = true) + { + $sXPath = "fields/field"; + if ($bFlattenLayers) + { + $sXPath = "fields/field[not(@_operation) or @_operation!='removed']"; + } + return $this->_priv_GetNodes($sXPath, $oClassNode); + } + + public function AddField(DOMNode $oClassNode, $sFieldCode, $sFieldType, $sSQL, $defaultValue, $bIsNullAllowed, $aExtraParams) + { + $oNewField = $this->oDOMDocument->createElement('field'); + $oNewField->setAttribute('name', $sFieldCode); + $this->_priv_AlterField($oNewField, $sFieldType, $sSQL, $defaultValue, $bIsNullAllowed, $aExtraParams); + $oFields = $oClassNode->getElementsByTagName('fields')->item(0); + $oFields->AppendChild($oNewField); + $this->_priv_SetFlag($oNewField, 'created'); + } + + public function RemoveField(DOMNode $oClassNode, $sFieldCode) + { + $sXPath = "fields/field[@name='$sFieldCode']"; + $oFieldNodes = $this->_priv_GetNodes($sXPath, $oClassNode); + if (is_object($oFieldNodes) && (is_object($oFieldNodes->item(0)))) + { + //@@TODO: if the field was 'created' => then really delete it + $this->_priv_SetFlag($oFieldNodes->item(0), 'removed'); + } + } + + public function AlterField(DOMNode $oClassNode, $sFieldCode, $sFieldType, $sSQL, $defaultValue, $bIsNullAllowed, $aExtraParams) + { + $sXPath = "fields/field[@name='$sFieldCode']"; + $oFieldNodes = $this->_priv_GetNodes($sXPath, $oClassNode); + if (is_object($oFieldNodes) && (is_object($oFieldNodes->item(0)))) + { + //@@TODO: if the field was 'created' => then let it as 'created' + $this->_priv_SetFlag($oFieldNodes->item(0), 'modified'); + $this->_priv_AlterField($oFieldNodes->item(0), $sFieldType, $sSQL, $defaultValue, $bIsNullAllowed, $aExtraParams); + } + } + + protected function _priv_AlterField(DOMNode $oFieldNode, $sFieldType, $sSQL, $defaultValue, $bIsNullAllowed, $aExtraParams) + { + switch($sFieldType) + { + case 'String': + case 'Text': + case 'IPAddress': + case 'EmailAddress': + case 'Blob': + break; + + case 'ExternalKey': + $this->_priv_AddFieldAttribute($oFieldNode, 'target_class', $aExtraParams); + // Fall through + case 'HierarchicalKey': + $this->_priv_AddFieldAttribute($oFieldNode, 'on_target_delete', $aExtraParams); + $this->_priv_AddFieldAttribute($oFieldNode, 'filter', $aExtraParams); + break; + + case 'ExternalField': + $this->_priv_AddFieldAttribute($oFieldNode, 'extkey_attcode', $aExtraParams); + $this->_priv_AddFieldAttribute($oFieldNode, 'target_attcode', $aExtraParams); + break; + + case 'Enum': + $this->_priv_SetFieldValues($oFieldNode, $aExtraParams); + break; + + case 'LinkedSetIndirect': + $this->_priv_AddFieldAttribute($oFieldNode, 'ext_key_to_remote', $aExtraParams); + // Fall through + case 'LinkedSet': + $this->_priv_AddFieldAttribute($oFieldNode, 'linked_class', $aExtraParams); + $this->_priv_AddFieldAttribute($oFieldNode, 'ext_key_to_me', $aExtraParams); + $this->_priv_AddFieldAttribute($oFieldNode, 'count_min', $aExtraParams); + $this->_priv_AddFieldAttribute($oFieldNode, 'count_max', $aExtraParams); + break; + + default: + throw(new Exception('Unsupported type of field: '.$sFieldType)); + } + $this->_priv_SetFieldDependencies($oFieldNode, $aExtraParams); + $oFieldNode->setAttribute('type', $sFieldType); + $oFieldNode->setAttribute('sql', $sSQL); + $oFieldNode->setAttribute('default_value', $defaultValue); + $oFieldNode->setAttribute('is_null_alllowed', $bIsNullAllowed ? 'true' : 'false'); + } + + protected function _priv_AddFieldAttribute(DOMNode $oFieldNode, $sAttributeCode, $aExtraParams, $bMandatory = false) + { + $value = array_key_exists($sAttributeCode, $aExtraParams) ? $aExtraParams[$sAttributeCode] : ''; + if (($value == '') && (!$bMandatory)) return; + $oFieldNode->setAttribute($sAttributeCode, $value); + } + + protected function _priv_SetFieldDependencies($oFieldNode, $aExtraParams) + { + $aDeps = array_key_exists('dependencies', $aExtraParams) ? $aExtraParams['dependencies'] : ''; + $oDependencies = $oFieldNode->getElementsByTagName('dependencies')->item(0); + + // No dependencies before, and no dependencies to add, exit + if (($oDependencies == null) && ($aDeps == '')) return; + + // Remove the previous dependencies + $oFieldNode->removeChild($oDependencies); + // If no dependencies, exit + if ($aDeps == '') return; + + // Build the new list of dependencies + $oDependencies = $this->oDOMDocument->createElement('dependencies'); + + foreach($aDeps as $sAttCode) + { + $oDep = $this->oDOMDocument->createElement('attribute'); + $oDep->setAttribute('name', $sAttCode); + $oDependencies->addChild($oDep); + } + $oFieldNode->addChild($oDependencies); + } + + protected function _priv_SetFieldValues($oFieldNode, $aExtraParams) + { + $aVals = array_key_exists('values', $aExtraParams) ? $aExtraParams['values'] : ''; + $oValues = $oFieldNode->getElementByTagName('values')->item(0); + + // No dependencies before, and no dependencies to add, exit + if (($oValues == null) && ($aVals == '')) return; + + // Remove the previous dependencies + $oFieldNode->removeChild($oValues); + // If no dependencies, exit + if ($aVals == '') return; + + // Build the new list of dependencies + $oValues = $this->oDOMDocument->createElement('values'); + + foreach($aVals as $sValue) + { + $oVal = $this->oDOMDocument->createElement('value', $sValue); + $oValues->addChild($oVal); + } + $oFieldNode->addChild($oValues); + } + + protected function _priv_SetFlag($oNode, $sFlagValue) + { + $oNode->setAttribute('_operation', $sFlagValue); + if ($oNode->tagName != 'class') + { + $this->_priv_SetFlag($oNode->parentNode, 'modified'); + } + } + /** + * List all transitions from a given state + * @param DOMNode $oStateNode The state + * @param bool $bFlattenLayers + * @throws Exception + */ + public function ListTransitions(DOMNode $oStateNode, $bFlattenLayers = true) + { + $sXPath = "transitions/transition"; + if ($bFlattenLayers) + { + //$sXPath = "transitions/transition[@_operation!='removed']"; + } + return $this->_priv_GetNodes($sXPath, $oStateNode); + } + + /** + * List all states of a given class + * @param DOMNode $oClassNode The class + * @param bool $bFlattenLayers + * @throws Exception + */ + public function ListStates(DOMNode $oClassNode, $bFlattenLayers = true) + { + $sXPath = "lifecycle/states/state"; + if ($bFlattenLayers) + { + //$sXPath = "lifecycle/states/state[@_operation!='removed']"; + } + return $this->_priv_GetNodes($sXPath, $oClassNode); + } + + /** + * List Zlists from the DOM for a given class + * @param bool $bFlattenLayers + * @throws Exception + */ + public function ListZLists(DOMNode $oClassNode, $bFlattenLayers = true) + { + // Not yet implemented !!! + return array(); + } + + public function ApplyChanges() + { + $oNodes = $this->ListChanges(); + foreach($oNodes as $oClassNode) + { + $sOperation = $oClassNode->GetAttribute('_operation'); + switch($sOperation) + { + case 'added': + case 'modified': + // marked as added or modified, just reset the flag + $oClassNode->setAttribute('_operation', ''); + break; + + case 'removed': + // marked as deleted, let's remove the node from the tree + $oParent = $oClassNode->parentNode; + $sClass = $oClassNode->GetAttribute('name'); + echo "Calling removeChild...
"; + $oParent->removeChild($oClassNode); + unset(self::$aLoadedClasses[$sClass]); + break; + } + } + } + + public function ListChanges() + { + $sXPath = "//*[@_operation!='']"; + return $this->_priv_GetNodes($sXPath); + } + + /** + * Get the text/XML version of the delta + */ + public function GetDelta() + { + $oDelta = new DOMDocument('1.0', 'UTF-8'); + $oRootNode = $oDelta->createElement('classes'); + $oDelta->appendChild($oRootNode); + + $this->_priv_ImportModifiedChildren($oDelta, $oRootNode, $this->oDOMDocument->firstChild); + //file_put_contents($sXMLDestPath, $oDelta->saveXML()); + return $oDelta->saveXML(); + } + + + protected function _priv_ImportModifiedChildren(DOMDocument $oDoc, DOMNode $oDestNode, DOMNode $oNode) + { + static $iDepth = 0; + $iDepth++; + foreach($oNode->childNodes as $oChildNode) + { + $sOperation = $oChildNode->getAttribute('_operation'); + switch($sOperation) + { + case 'removed': + $oDeletedNode = $oDoc->importNode($oChildNode->cloneNode(false)); // Copies all the node's attributes, but NOT the child nodes + $oDeletedNode->removeAttribute('_source'); + if ($oDeletedNode->tagName == 'class') + { + // classes are always located under the root node + $oDoc->firstChild->appendChild($oDeletedNode); + } + else + { + $oDestNode->appendChild($oDeletedNode); + } +//echo "

".str_repeat('+', $iDepth).$oChildNode->getAttribute('name')." was removed...

"; + break; + + case 'modified': + case 'created': + //echo "

".str_repeat('+', $iDepth).$oChildNode->tagName.':'.$oChildNode->getAttribute('name')." was modified...

"; + $oModifiedNode = $oDoc->importNode($oChildNode->cloneNode(false)); // Copies all the node's attributes, but NOT the child nodes + $oModifiedNode->removeAttribute('_source'); + $this->_priv_ImportModifiedChildren($oDoc, $oModifiedNode, $oChildNode); + if ($oModifiedNode->tagName == 'class') + { + // classes are always located under the root node + $oDoc->firstChild->appendChild($oModifiedNode); + } + else + { + $oDestNode->appendChild($oModifiedNode); + } + break; + + default: + // No change: search if there is not a modified child class + $oModifiedNode = $oDoc->importNode($oChildNode->cloneNode(false)); // Copies all the node's attributes, but NOT the child nodes + $oModifiedNode->removeAttribute('_source'); + if ($oChildNode->tagName == 'class') + { +//echo "

".str_repeat('+', $iDepth)."Checking if a subclass of ".$oChildNode->getAttribute('name')." was modified...

"; + // classes are always located under the root node + $this->_priv_ImportModifiedChildren($oDoc, $oModifiedNode, $oChildNode); + } + } + } + $iDepth--; + } + /** + * Searches on disk in the root directory for module description files + * and returns an array of MFModules + * @return array Array of MFModules + */ + public function FindModules($sSubDirectory = '') + { + $aAvailableModules = ModuleDiscovery::GetAvailableModules($this->sRootDir, $sSubDirectory); + $aResult = array(); + foreach($aAvailableModules as $sId => $aModule) + { + $aResult[] = new MFModule($sId, $aModule['root_dir'], $aModule['label']); + } + return $aResult; + } + + /** + * Produce the PHP files corresponding to the data model + * @param $bTestOnly bool + * @return array Array of errors, empty if no error + */ + public function Compile($bTestOnly) + { + return array(); + } + + /** + * Extracts some nodes from the DOM + * @param string $sXPath A XPath expression + * @return DOMNodeList + */ + public function _priv_GetNodes($sXPath, $oContextNode = null) + { + $oXPath = new DOMXPath($this->oDOMDocument); + + if (is_null($oContextNode)) + { + return $oXPath->query($sXPath); + } + else + { + return $oXPath->query($sXPath, $oContextNode); + } + } + + /** + * Insert a new node in the DOM + * @param DOMNode $oNode + * @param DOMNode $oParentNode + */ + public function _priv_AddNode(DOMNode $oNode, DOMNode $oParentNode) + { + } + + /** + * Remove a node from the DOM + * @param DOMNode $oNode + * @param DOMNode $oParentNode + */ + public function _priv_RemoveNode(DOMNode $oNode) + { + } + + /** + * Add or modify an attribute of a node + * @param DOMNode $oNode + * @param string $sAttributeName + * @param mixed $atttribueValue + */ + public function _priv_SetNodeAttribute(DOMNode $oNode, $sAttributeName, $atttribueValue) + { + } +} \ No newline at end of file diff --git a/setup/modulediscovery.class.inc.php b/setup/modulediscovery.class.inc.php new file mode 100644 index 000000000..6f0dfc04f --- /dev/null +++ b/setup/modulediscovery.class.inc.php @@ -0,0 +1,274 @@ + 'One line description shown during the interactive setup', + 'dependencies' => 'array of module ids', + 'mandatory' => 'boolean', + 'visible' => 'boolean', + 'datamodel' => 'array of data model files', + //'dictionary' => 'array of dictionary files', // No longer mandatory, now automated + 'data.struct' => 'array of structural data files', + 'data.sample' => 'array of sample data files', + 'doc.manual_setup' => 'url', + 'doc.more_information' => 'url', + ); + + + // Cache the results and the source directory + // Note that, as class can be declared within the module files, they cannot be loaded twice. + // Then the following assumption is made: within the same execution page, the module + // discovery CANNOT be executed on several different paths + protected static $m_sModulesRoot = null; + protected static $m_aModules = array(); + + // All the entries below are list of file paths relative to the module directory + protected static $m_aFilesList = array('datamodel', 'webservice', 'dictionary', 'data.struct', 'data.sample'); + + + // ModulePath is used by AddModule to get the path of the module being included (in ListModuleFiles) + protected static $m_sModulePath = null; + protected static function SetModulePath($sModulePath) + { + self::$m_sModulePath = $sModulePath; + } + + public static function AddModule($sFilePath, $sId, $aArgs) + { + if (!array_key_exists('itop_version', $aArgs)) + { + // Assume 1.0.2 + $aArgs['itop_version'] = '1.0.2'; + } + foreach (self::$m_aModuleArgs as $sArgName => $sArgDesc) + { + if (!array_key_exists($sArgName, $aArgs)) + { + throw new Exception("Module '$sId': missing argument '$sArgName'"); + } + } + + $aArgs['root_dir'] = dirname($sFilePath); + + self::$m_aModules[$sId] = $aArgs; + + foreach(self::$m_aFilesList as $sAttribute) + { + if (isset(self::$m_aModules[$sId][$sAttribute])) + { + // All the items below are list of files, that are relative to the current file + // being loaded, let's update their path to store path relative to the application directory + foreach(self::$m_aModules[$sId][$sAttribute] as $idx => $sRelativePath) + { + self::$m_aModules[$sId][$sAttribute][$idx] = self::$m_sModulePath.'/'.$sRelativePath; + } + } + } + // Populate automatically the list of dictionary files + if(preg_match('|^([^/]+)|', $sId, $aMatches)) // ModuleName = everything before the first forward slash + { + $sModuleName = $aMatches[1]; + $sDir = dirname($sFilePath); + if ($hDir = opendir($sDir)) + { + while (($sFile = readdir($hDir)) !== false) + { + $aMatches = array(); + if (preg_match("/^[^\\.]+.dict.$sModuleName.php$/i", $sFile, $aMatches)) // Dictionary files named like .dict..php are loaded automatically + { + self::$m_aModules[$sId]['dictionary'][] = self::$m_sModulePath.'/'.$sFile; + } + } + closedir($hDir); + } + } + } + + protected static function GetModules($oP = null) + { + // Order the modules to take into account their inter-dependencies + $aDependencies = array(); + foreach(self::$m_aModules as $sId => $aModule) + { + $aDependencies[$sId] = $aModule['dependencies']; + } + $aOrderedModules = array(); + $iLoopCount = 1; + while(($iLoopCount < count(self::$m_aModules)) && (count($aDependencies) > 0) ) + { + foreach($aDependencies as $sId => $aRemainingDeps) + { + $bDependenciesSolved = true; + foreach($aRemainingDeps as $sDepId) + { + if (!in_array($sDepId, $aOrderedModules)) + { + $bDependenciesSolved = false; + } + } + if ($bDependenciesSolved) + { + $aOrderedModules[] = $sId; + unset($aDependencies[$sId]); + } + } + $iLoopCount++; + } + if (count($aDependencies) >0) + { + $sHtml = "
    Warning: the following modules have unmet dependencies, and have been ignored:\n"; + foreach($aDependencies as $sId => $aDeps) + { + $aModule = self::$m_aModules[$sId]; + $sHtml.= "
  • {$aModule['label']} (id: $sId), depends on: ".implode(', ', $aDeps)."
  • "; + } + $sHtml .= "
\n"; + if ($oP instanceof SetupPage) + { + $oP->warning($sHtml); // used in the context of the installation + } + elseif (class_exists('SetupPage')) + { + SetupPage::log_warning($sHtml); // used in the context of ? + } + else + { + echo $sHtml; // used in the context of the compiler + } + } + // Return the ordered list, so that the dependencies are met... + $aResult = array(); + foreach($aOrderedModules as $sId) + { + $aResult[$sId] = self::$m_aModules[$sId]; + } + return $aResult; + } + + /** + * Search (on the disk) for all defined iTop modules, load them and returns the list (as an array) + * of the possible iTop modules to install + * @param sRootDir Application root directory + * @param sSearchDir Directory to search (relative to root dir) + * @return Hash A big array moduleID => ModuleData + */ + public static function GetAvailableModules($sRootDir, $sSearchDir, $oP = null) + { + $sLookupDir = realpath($sRootDir.'/'.$sSearchDir); + + if (is_null(self::$m_sModulesRoot)) + { + // First call + // + if ($sLookupDir == '') + { + throw new Exception("Invalid directory '$sRootDir/$sSearchDir'"); + } + self::$m_sModulesRoot = $sLookupDir; + + clearstatcache(); + self::ListModuleFiles($sSearchDir, $sRootDir); + return self::GetModules($oP); + } + elseif (self::$m_sModulesRoot != $sLookupDir) + { + throw new Exception("Design issue: the discovery of modules cannot be made on two different paths (previous: ".self::$m_sModulesRoot.", new: $sLookupDir)"); + } + else + { + // Reuse the previous results + // + return self::GetModules($oP); + } + } + + /** + * Helper function to interpret the name of a module + * @param $sModuleId string Identifier of the module, in the form 'name/version' + * @return array(name, version) + */ + public static function GetModuleName($sModuleId) + { + if (preg_match('!^(.*)/(.*)$!', $sModuleId, $aMatches)) + { + $sName = $aMatches[1]; + $sVersion = $aMatches[2]; + } + else + { + $sName = $sModuleId; + $sVersion = ""; + } + return array($sName, $sVersion); + } + + /** + * Helper function to browse a directory and get the modules + * @param $sRelDir string Directory to start from + * @return array(name, version) + */ + protected static function ListModuleFiles($sRelDir, $sRootDir) + { + $sDirectory = $sRootDir.'/'.$sRelDir; + //echo "

$sDirectory

\n"; + if ($hDir = opendir($sDirectory)) + { + // This is the correct way to loop over the directory. (according to the documentation) + while (($sFile = readdir($hDir)) !== false) + { + $aMatches = array(); + if (is_dir($sDirectory.'/'.$sFile)) + { + if (($sFile != '.') && ($sFile != '..') && ($sFile != '.svn')) + { + self::ListModuleFiles($sRelDir.'/'.$sFile, $sRootDir); + } + } + else if (preg_match('/^module\.(.*).php$/i', $sFile, $aMatches)) + { + self::SetModulePath($sRelDir); + try + { + //echo "

Loading: $sDirectory/$sFile...

\n"; + //SetupPage::log_info("Discovered module $sFile"); + require_once($sDirectory.'/'.$sFile); + //echo "

Done.

\n"; + } + catch(Exception $e) + { + // Continue... + } + } + } + closedir($hDir); + } + else + { + throw new Exception("Data directory (".$sDirectory.") not found or not readable."); + } + } +} // End of class + + +/** Alias for backward compatibility with old module files in which + * the declaration of a module invokes SetupWebPage::AddModule() + * whereas the new form is ModuleDiscovery::AddModule() + */ +class SetupWebPage extends ModuleDiscovery{} + +?> diff --git a/setup/setuppage.class.inc.php b/setup/setuppage.class.inc.php index 87117b4f5..127ad1387 100644 --- a/setup/setuppage.class.inc.php +++ b/setup/setuppage.class.inc.php @@ -24,7 +24,7 @@ */ require_once(APPROOT."/application/nicewebpage.class.inc.php"); -require_once(APPROOT."designer/modulediscovery.class.inc.php"); +require_once(APPROOT."setup/modulediscovery.class.inc.php"); define('INSTALL_LOG_FILE', APPROOT.'/setup.log');