Przeglądaj źródła

Core: a module can have its own design defined in XML (/itop_design/modules_designs/module_design) and accessed at run time via the class ModuleDesign. Switching to XML version 1.3.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3820 a333f486-631f-4898-b8df-5754b55c2be0
romainq 9 lat temu
rodzic
commit
7eb1370be9

+ 340 - 0
core/moduledesign.class.inc.php

@@ -0,0 +1,340 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+/**
+ * Module specific customizations:
+ * The customizations are done in XML, within a module_design section (itop_design/module_designs/module_design)
+ * The module reads the cusomtizations by the mean of the ModuleDesign API
+ * @package Core
+ */
+
+require_once(APPROOT.'application/utils.inc.php');
+
+
+/**
+ * Class ModuleDesign
+ *
+ * Usage from within a module:
+ *
+ * // Fetch the design
+ * $oDesign = new ModuleDesign('tagada');
+ *
+ * // Read data from the root node
+ * $oRoot = $oDesign->documentElement;
+ * $oProperties = $oRoot->GetUniqueElement('properties');
+ * $prop1 = $oProperties->GetChildText('property1');
+ * $prop2 = $oProperties->GetChildText('property2');
+ *
+ * // Read data by searching the entire DOM
+ * foreach ($oDesign->GetNodes('/module_design/bricks/brick') as $oBrickNode)
+ * {
+ *   $sId = $oBrickNode->getAttribute('id');
+ *   $sType = $oBrickNode->getAttribute('xsi:type');
+ * }
+ *
+ * // Search starting a given node
+ * $oBricks = $oDesign->documentElement->GetUniqueElement('bricks');
+ * foreach ($oBricks->GetNodes('brick') as $oBrickNode)
+ * {
+ *   ...
+ * }
+ */
+class ModuleDesign extends DOMDocument
+{
+	/**
+	 * @param string|null $sDesignSourceId Identifier of the section module_design (generally a module name), null to build an empty design
+	 * @throws Exception
+	 */
+	public function __construct($sDesignSourceId = null)
+	{
+		parent::__construct('1.0', 'UTF-8');
+		$this->Init();
+
+		if (!is_null($sDesignSourceId))
+		{
+			$this->LoadFromCompiledDesigns($sDesignSourceId);
+		}
+	}
+
+	/**
+	 * Overloadable. Called prior to data loading.
+	 */
+	protected function Init()
+	{
+		$this->registerNodeClass('DOMElement', 'ModuleDesignElement');
+
+		$this->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
+		$this->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
+	}
+
+	/**
+	 * Gets the data where the compiler has left them...
+	 * @param $sDesignSourceId Identifier of the section module_design (generally a module name)
+	 * @throws Exception
+	 */
+	protected function LoadFromCompiledDesigns($sDesignSourceId)
+	{
+		$sDesignDir = APPROOT.'env-'.utils::GetCurrentEnvironment().'/core/module_designs/';
+		$sFile = $sDesignDir.$sDesignSourceId.'.xml';
+		if (!file_exists($sFile))
+		{
+			$aFiles = glob($sDesignDir.'/*.xml');
+			if (count($aFiles) == 0)
+			{
+				$sAvailable = 'none!';
+			}
+			else
+			{
+				var_dump($aFiles);
+				$aAvailable = array();
+				foreach ($aFiles as $sFile)
+				{
+					$aAvailable[] = "'".basename($sFile, '.xml')."'";
+				}
+				$sAvailable = implode(', ', $aAvailable);
+			}
+			throw new Exception("Could not load module design '$sDesignSourceId'. Available designs: $sAvailable");
+		}
+
+		// Silently keep track of errors
+		libxml_use_internal_errors(true);
+		libxml_clear_errors();
+		$this->load($sFile);
+		//$bValidated = $oDocument->schemaValidate(APPROOT.'setup/itop_design.xsd');
+		$aErrors = libxml_get_errors();
+		if (count($aErrors) > 0)
+		{
+			$aDisplayErrors = array();
+			foreach($aErrors as $oXmlError)
+			{
+				$aDisplayErrors[] = 'Line '.$oXmlError->line.': '.$oXmlError->message;
+			}
+
+			throw new Exception("Invalid XML in '$sFile'. Errors: ".implode(', ', $aDisplayErrors));
+		}
+	}
+
+	/**
+	 * Overload of the standard API
+	 */
+	public function load($filename, $options = 0)
+	{
+		parent::load($filename, LIBXML_NOBLANKS);
+	}
+
+	/**
+	 * Overload of the standard API
+	 */
+	public function save($filename, $options = 0)
+	{
+		$this->documentElement->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
+		return parent::save($filename, LIBXML_NOBLANKS);
+	}
+
+	/**
+	 * Create an HTML representation of the DOM, for debugging purposes
+	 * @param bool|false $bReturnRes Echoes or returns the HTML representation
+	 * @return mixed void or the HTML representation of the DOM
+	 */
+	public function Dump($bReturnRes = false)
+	{
+		$sXml = $this->saveXML();
+		if ($bReturnRes)
+		{
+			return $sXml;
+		}
+		else
+		{
+			echo "<pre>\n";
+			echo htmlentities($sXml);
+			echo "</pre>\n";
+		}
+	}
+
+	/**
+	 * Quote and escape strings for use within an XPath expression
+	 * Usage: DesignDocument::GetNodes('class[@id='.DesignDocument::XPathQuote($sId).']');
+	 * @param $sValue The value to be quoted
+	 * @return string to be used within an XPath
+	 */
+	public static function XPathQuote($sValue)
+	{
+		if (strpos($sValue, '"') !== false)
+		{
+			$aParts = explode('"', $sValue);
+			$sRet = 'concat("'.implode('", \'"\', "', $aParts).'")';
+		}
+		else
+		{
+			$sRet = '"'.$sValue.'"';
+		}
+		return $sRet;
+	}
+
+	/**
+	 * Extracts some nodes from the DOM
+	 * @param string $sXPath A XPath expression
+	 * @param DesignNode|null $oContextNode The node to start the search from
+	 * @return DOMNodeList
+	 */
+	public function GetNodes($sXPath, $oContextNode = null)
+	{
+		$oXPath = new DOMXPath($this);
+		if (is_null($oContextNode))
+		{
+			$oResult = $oXPath->query($sXPath);
+		}
+		else
+		{
+			$oResult = $oXPath->query($sXPath, $oContextNode);
+		}
+		return $oResult;
+	}
+
+	/**
+	 * An alternative to getNodePath, that gives the id of nodes instead of the position within the children
+	 */
+	public static function GetItopNodePath($oNode)
+	{
+		if ($oNode instanceof DOMDocument) return '';
+
+		$sId = $oNode->getAttribute('id');
+		$sNodeDesc = ($sId != '') ? $oNode->nodeName.'['.$sId.']' : $oNode->nodeName;
+		return self::GetItopNodePath($oNode->parentNode).'/'.$sNodeDesc;
+	}
+}
+
+
+/**
+ * ModuleDesignElement: helper to read/change the DOM
+ * @package ModelFactory
+ */
+class ModuleDesignElement extends DOMElement
+{
+	/**
+	 * Extracts some nodes from the DOM
+	 * @param string $sXPath A XPath expression
+	 * @return DOMNodeList
+	 */
+	public function GetNodes($sXPath)
+	{
+		return $this->ownerDocument->GetNodes($sXPath, $this);
+	}
+
+	/**
+	 * Create an HTML representation of the DOM, for debugging purposes
+	 * @param bool|false $bReturnRes Echoes or returns the HTML representation
+	 * @return mixed void or the HTML representation of the DOM
+	 */
+	public function Dump($bReturnRes = false)
+	{
+		$oDoc = new iTopDesignDocument();
+		$oClone = $oDoc->importNode($this->cloneNode(true), true);
+		$oDoc->appendChild($oClone);
+
+		$sXml = $oDoc->saveXML($oClone);
+		if ($bReturnRes)
+		{
+			return $sXml;
+		}
+		else
+		{
+			echo "<pre>\n";
+			echo htmlentities($sXml);
+			echo "</pre>\n";
+		}
+	}
+
+	/**
+	 * Returns the node directly under the given node
+	 * @param $sTagName
+	 * @param bool|true $bMustExist
+	 * @return null
+	 * @throws DOMFormatException
+	 */
+	public function GetUniqueElement($sTagName, $bMustExist = true)
+	{
+		$oNode = null;
+		foreach($this->childNodes as $oChildNode)
+		{
+			if ($oChildNode->nodeName == $sTagName)
+			{
+				$oNode = $oChildNode;
+				break;
+			}
+		}
+		if ($bMustExist && is_null($oNode))
+		{
+			throw new DOMFormatException('Missing unique tag: '.$sTagName);
+		}
+		return $oNode;
+	}
+
+	/**
+	 * Returns the node directly under the current node, or null if missing
+	 * @param $sTagName
+	 * @return null
+	 * @throws DOMFormatException
+	 */
+	public function GetOptionalElement($sTagName)
+	{
+		return $this->GetUniqueElement($sTagName, false);
+	}
+
+	/**
+	 * Returns the TEXT of the current node (possibly from several child nodes)
+	 * @param null $sDefault
+	 * @return null|string
+	 */
+	public function GetText($sDefault = null)
+	{
+		$sText = null;
+		foreach($this->childNodes as $oChildNode)
+		{
+			if ($oChildNode instanceof DOMText)
+			{
+				if (is_null($sText)) $sText = '';
+				$sText .= $oChildNode->wholeText;
+			}
+		}
+		if (is_null($sText))
+		{
+			return $sDefault;
+		}
+		else
+		{
+			return $sText;
+		}
+	}
+
+	/**
+	 * Get the TEXT value from a child node
+	 * @param string $sTagName
+	 * @param string|null $sDefault
+	 * @return string
+	 */
+	public function GetChildText($sTagName, $sDefault = null)
+	{
+		$sRet = $sDefault;
+		if ($oChild = $this->GetOptionalElement($sTagName))
+		{
+			$sRet = $oChild->GetText($sDefault);
+		}
+		return $sRet;
+	}
+}

+ 20 - 4
setup/compiler.class.inc.php

@@ -18,6 +18,8 @@
 
 
 require_once(APPROOT.'setup/setuputils.class.inc.php');
+require_once(APPROOT.'core/moduledesign.class.inc.php');
+
 
 class DOMFormatException extends Exception
 {
@@ -429,7 +431,10 @@ EOF;
 		// Compile the portals
 		$oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0);
 		$this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir);
-		
+
+		// Create module design XML files
+		$this->CompileModuleDesigns($sTempTargetDir, $sFinalTargetDir);
+
 		// Compile the XML parameters
 		$oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0);
 		$this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir);
@@ -2221,6 +2226,19 @@ EOF;
 		}
 	}
 	
+	protected function CompileModuleDesigns($sTempTargetDir, $sFinalTargetDir)
+	{
+		SetupUtils::builddir($sTempTargetDir.'/core/module_designs');
+		$oDesigns = $this->oFactory->GetNodes('/itop_design/module_designs/module_design');
+		foreach($oDesigns as $oDesign)
+		{
+			$oDoc = new ModuleDesign();
+			$oClone = $oDoc->importNode($oDesign->cloneNode(true), true);
+			$oDoc->appendChild($oClone);
+			$oDoc->save($sTempTargetDir.'/core/module_designs/'.$oDesign->getAttribute('id').'.xml');
+		}
+	}
+
 	protected function LoadSnippets()
 	{
 		$oSnippets = $this->oFactory->GetNodes('/itop_design/snippets/snippet');
@@ -2252,7 +2270,7 @@ EOF;
 			{
 				$this->aSnippets[$sModuleId] = array('before' => array(), 'after' => array());
 			}
-			
+
 			$fOrder = (float) $oSnippet->GetChildText('rank', 0);
 			$sContent = $oSnippet->GetChildText('content', '');
 			if ($fOrder < 0)
@@ -2278,5 +2296,3 @@ EOF;
 		}
 	}
 }
-
-?>

+ 31 - 1
setup/itopdesignformat.class.inc.php

@@ -35,7 +35,7 @@
  * }
  */
  
-define('ITOP_DESIGN_LATEST_VERSION', '1.2');
+define('ITOP_DESIGN_LATEST_VERSION', '1.3'); // iTop > 2.2.0
  
 class iTopDesignFormat
 {
@@ -55,6 +55,12 @@ class iTopDesignFormat
 		'1.2' => array(
 			'previous' => '1.1',
 			'go_to_previous' => 'From12To11',
+			'next' => '1.3',
+			'go_to_next' => 'From12To13',
+		),
+		'1.3' => array(
+			'previous' => '1.2',
+			'go_to_previous' => 'From13To12',
 			'next' => null,
 			'go_to_next' => null,
 		),
@@ -474,6 +480,30 @@ class iTopDesignFormat
 	}
 
 	/**
+	 * Upgrade the format from version 1.2 to 1.3
+	 * @return void (Errors are logged)
+	 */
+	protected function From12To13($oFactory)
+	{
+	}
+
+	/**
+	 * Downgrade the format from version 1.3 to 1.2
+	 * @return void (Errors are logged)
+	 */
+	protected function From13To12($oFactory)
+	{
+		$oXPath = new DOMXPath($this->oDocument);
+
+		$oNodeList = $oXPath->query('/itop_design/module_designs/module_design');
+		foreach ($oNodeList as $oNode)
+		{
+			$this->LogWarning('The module design defined in '.self::GetItopNodePath($oNode).' will be lost.');
+			$this->DeleteNode($oNode);
+		}
+	}
+
+	/**
 	 * Delete a node from the DOM and make sure to also remove the immediately following line break (DOMText), if any.
 	 * This prevents generating empty lines in the middle of the XML
 	 * @param DOMNode $oNode