Text
D1
D2
first a
first a
blah
EOF;
$this->oDOMDocument = new MFDocument();
$this->oDOMDocument->loadXML($sOriginalXML);
// DOM Get the original values, then modify its contents by the mean of the API
$oRoot = $this->GetNodes('//itop_design')->item(0);
//$oRoot->Dump();
$sDOMOriginal = $oRoot->Dump(true);
$oNode = $oRoot->GetNodes('a/b')->item(0);
$oNew = $this->oDOMDocument->CreateElement('b', 'New text');
$oNode->parentNode->RedefineChildNode($oNew);
$oNode = $oRoot->GetNodes('a/c')->item(0);
$oNewC = $this->oDOMDocument->CreateElement('c');
$oNewC->setAttribute('id', '1');
$oNode->parentNode->RedefineChildNode($oNewC);
$oNewC->appendChild($this->oDOMDocument->CreateElement('d', 'x'));
$oNewC->appendChild($this->oDOMDocument->CreateElement('d', 'y'));
$oNewC->appendChild($this->oDOMDocument->CreateElement('d', 'z'));
$oNamedNode = $this->oDOMDocument->CreateElement('z');
$oNamedNode->setAttribute('id', 'abc');
$oNewC->AddChildNode($oNamedNode);
$oNewC->AddChildNode($this->oDOMDocument->CreateElement('r', 'to be replaced'));
// Alter this "modified node", no flag should be set in its subnodes
$oNewC->Rename('blah');
$oNewC->Rename('foo');
$oNewC->AddChildNode($this->oDOMDocument->CreateElement('y', '(no flag)'));
$oNewC->AddChildNode($this->oDOMDocument->CreateElement('x', 'To delete programatically'));
$oSubNode = $oNewC->GetUniqueElement('z');
$oSubNode->Rename('abcdef');
$oSubNode = $oNewC->GetUniqueElement('x');
$oSubNode->Delete();
$oNewC->RedefineChildNode($this->oDOMDocument->CreateElement('r', 'replacement'));
$oNode = $oRoot->GetNodes("//a[@id='second a']")->item(0);
$oNode->Rename('el 2o A');
$oNode->Rename('el secundo A');
$oNew = $this->oDOMDocument->CreateElement('e', 'Something new here');
$oNode->AddChildNode($oNew);
$oNewA = $this->oDOMDocument->CreateElement('a');
$oNewA->setAttribute('id', 'new a');
$oNode->AddChildNode($oNewA);
$oSubnode = $this->oDOMDocument->CreateElement('parent', 'el secundo A');
$oSubnode->setAttribute('id', 'to be changed');
$oNewA->AddChildNode($oSubnode);
$oNewA->AddChildNode($this->oDOMDocument->CreateElement('f', 'Welcome to the newcomer'));
$oNewA->AddChildNode($this->oDOMDocument->CreateElement('x', 'To delete programatically'));
// Alter this "new a", as it is new, no flag should be set
$oNewA->Rename('new_a');
$oSubNode = $oNewA->GetUniqueElement('parent');
$oSubNode->Rename('alter ego');
$oNewA->RedefineChildNode($this->oDOMDocument->CreateElement('f', 'dummy data'));
$oSubNode = $oNewA->GetUniqueElement('x');
$oSubNode->Delete();
$oNode = $oRoot->GetNodes("//a[@id='third a']")->item(0);
$oNode->Delete();
//$oRoot->Dump();
$sDOMModified = $oRoot->Dump(true);
// Compute the delta
//
$sDeltaXML = $this->GetDelta();
//echo "\n";
//echo htmlentities($sDeltaXML);
//echo "
\n";
// Reiterating - try to remake the DOM by applying the computed delta
//
$this->oDOMDocument = new MFDocument();
$this->oDOMDocument->loadXML($sOriginalXML);
$oRoot = $this->GetNodes('//itop_design')->item(0);
//$oRoot->Dump();
echo "Rebuild the DOM - Delta applied...
\n";
$oDeltaDoc = new MFDocument();
$oDeltaDoc->loadXML($sDeltaXML);
//$oDeltaDoc->Dump();
//$this->oDOMDocument->Dump();
$oDeltaRoot = $oDeltaDoc->childNodes->item(0);
$this->LoadDelta($oDeltaDoc, $oDeltaRoot, $this->oDOMDocument);
//$oRoot->Dump();
$sDOMRebuilt = $oRoot->Dump(true);
}
catch (Exception $e)
{
echo "Exception: ".$e->getMessage()."
\n";
echo "\n";
debug_print_backtrace();
echo "
\n";
}
$sArrStyle = "font-size: 40;";
echo "\n";
echo " \n";
echo " \n";
echo " DOM - Original values\n";
echo " ".htmlentities($sDOMOriginal)." \n";
echo " | \n";
echo " ⇒ ⇒ ⇒ | \n";
echo "
\n";
echo " ⇓ | ⇓ |
\n";
echo " \n";
echo " \n";
echo " DOM - Altered with various changes\n";
echo " ".htmlentities($sDOMModified)." \n";
echo " | \n";
echo " \n";
echo " DOM - Rebuilt from the Delta\n";
echo " ".htmlentities($sDOMRebuilt)." \n";
echo " | \n";
echo "
\n";
echo " ⇓ | ⇑ |
\n";
echo " \n";
echo " Delta (Computed by ModelFactory)\n";
echo " ".htmlentities($sDeltaXML)." \n";
echo " | \n";
echo " ⇒ ⇒ ⇒ | \n";
echo " \n";
echo "
\n";
} // TEST !
/**
* Extracts some nodes from the DOM
* @param string $sXPath A XPath expression
* @return DOMNodeList
*/
public function GetNodes($sXPath, $oContextNode = null)
{
return $this->oDOMDocument->GetNodes($sXPath, $oContextNode);
}
public function ListActiveChildNodes($sContextXPath, $sTagName)
{
$oContextPath = $this->oRoot->GetNodes($sContextXPath)->item(0);
return $oContextPath->ListActiveChildNodes($sTagName);
}
}
/**
* MFElement: helper to read/change the DOM
* @package ModelFactory
*/
class MFElement 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);
}
/**
* For debugging purposes - but this is currently buggy: the whole document is rendered
*/
public function Dump($bReturnRes = false)
{
$sXml = $this->ownerDocument->saveXML($this);
if ($bReturnRes)
{
return $sXml;
}
else
{
echo "\n";
echo htmlentities($sXml);
echo "
\n";
}
}
/**
* Returns the node directly under the given node
*/
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
*/
public function GetOptionalElement($sTagName)
{
return $this->GetUniqueElement($sTagName, false);
}
/**
* Returns the TEXT of the current node (possibly from several subnodes)
*/
public function GetText($sDefault = null)
{
$sText = null;
foreach($this->childNodes as $oChildNode)
{
if ($oChildNode instanceof DOMCharacterData) // Base class of DOMText and DOMCdataSection
{
if (is_null($sText)) $sText = '';
$sText .= $oChildNode->wholeText;
}
}
if (is_null($sText))
{
return $sDefault;
}
else
{
return $sText;
}
}
/**
* Get the TEXT value from the child node
*/
public function GetChildText($sTagName, $sDefault = null)
{
$sRet = $sDefault;
if ($oChild = $this->GetOptionalElement($sTagName))
{
$sRet = $oChild->GetText($sDefault);
}
return $sRet;
}
/**
* Assumes the current 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
*/
public function GetNodeAsArrayOfItems($sElementName = 'items')
{
$oItems = $this->GetOptionalElement($sElementName);
if ($oItems)
{
$res = array();
$aRanks = array();
foreach($oItems->childNodes as $oItem)
{
// When an attribute is missing
if ($oItem->hasAttribute('id'))
{
$key = $oItem->getAttribute('id');
if (array_key_exists($key, $res))
{
// Houston!
throw new DOMFormatException("Tag ".$oItem->getNodePath().", id '$key' already used!!!");
}
$res[$key] = $oItem->GetNodeAsArrayOfItems();
}
else
{
$res[] = $oItem->GetNodeAsArrayOfItems();
}
$sRank = $oItem->GetChildText('rank');
if ($sRank != '')
{
$aRanks[] = (float) $sRank;
}
else
{
$aRanks[] = count($aRanks) > 0 ? max($aRanks) + 1 : 0;
}
array_multisort($aRanks, $res);
}
}
else
{
$res = $this->GetText();
}
return $res;
}
public function SetNodeAsArrayOfItems($aList)
{
$oNewNode = $this->ownerDocument->CreateElement($this->tagName);
if ($this->getAttribute('id') != '')
{
$oNewNode->setAttribute('id', $this->getAttribute('id'));
}
self::AddItemToNode($this->ownerDocument, $oNewNode, $aList);
$this->parentNode->RedefineChildNode($oNewNode);
}
protected static function AddItemToNode($oXmlDoc, $oXMLNode, $itemValue)
{
if (is_array($itemValue))
{
$oXmlItems = $oXmlDoc->CreateElement('items');
$oXMLNode->AppendChild($oXmlItems);
foreach($itemValue as $key => $item)
{
$oXmlItem = $oXmlDoc->CreateElement('item');
$oXmlItems->AppendChild($oXmlItem);
if (is_string($key))
{
$oXmlItem->SetAttribute('key', $key);
}
self::AddItemToNode($oXmlDoc, $oXmlItem, $item);
}
}
else
{
$oXmlText = $oXmlDoc->CreateTextNode((string) $itemValue);
$oXMLNode->AppendChild($oXmlText);
}
}
/**
* Helper to remove child nodes
*/
public function DeleteChildren()
{
while (isset($this->firstChild))
{
if ($this->firstChild instanceof MFElement)
{
$this->firstChild->DeleteChildren();
}
$this->removeChild($this->firstChild);
}
}
/**
* Find the child node matching the given node
* @param MFElement $oRefNode The node to search for
* @param bool $sSearchId substitutes to the value of the 'id' attribute
*/
public function FindExistingChildNode(MFElement $oRefNode, $sSearchId = null)
{
return self::FindNode($this, $oRefNode, $sSearchId);
}
/**
* Seems to work fine (and is about 10 times faster than above) EXCEPT for menus !!!!
* @param unknown_type $oParent
* @param unknown_type $oRefNode
* @param unknown_type $sSearchId
* @throws Exception
*/
public static function FindNode(DOMNode $oParent, MFElement $oRefNode, $sSearchId = null)
{
$oRes = null;
if ($oParent instanceof DOMDocument)
{
$oDoc = $oParent->firstChild->ownerDocument;
$oRoot = $oParent;
}
else
{
$oDoc = $oParent->ownerDocument;
$oRoot = $oParent;
}
$oXPath = new DOMXPath($oDoc);
if ($oRefNode->hasAttribute('id'))
{
// Find the first element having the same tag name and id
if (!$sSearchId)
{
$sSearchId = $oRefNode->getAttribute('id');
}
$sXPath = './'.$oRefNode->tagName."[@id='$sSearchId']";
$oRes = $oXPath->query($sXPath, $oRoot)->item(0);
}
else
{
// Get the first one having the same tag name (ignore others)
$sXPath = './'.$oRefNode->tagName;
$oRes = $oXPath->query($sXPath, $oRoot)->item(0);
}
return $oRes;
}
public function ListActiveChildNodes($sTagName)
{
$sXPath = $sTagName."[not(@_alteration) or @_alteration!='removed']";
return $this->GetNodes($sXPath);
}
/**
* Check if the current node is under a node 'added' or 'altered'
* Usage: In such a case, the change must not be tracked
*/
protected function IsInDefinition()
{
// Iterate through the parents: reset the flag if any of them has a flag set
for($oParent = $this ; $oParent instanceof MFElement ; $oParent = $oParent->parentNode)
{
if ($oParent->getAttribute('_alteration') != '')
{
return true;
}
}
return false;
}
/**
* Add a node and set the flags that will be used to compute the delta
* @param MFElement $oNode The node (including all subnodes) to add
*/
public function AddChildNode(MFElement $oNode)
{
$oExisting = $this->FindExistingChildNode($oNode);
if ($oExisting)
{
if ($oExisting->getAttribute('_alteration') != 'removed')
{
throw new Exception("Attempting to add a node that already exists: $oNode->tagName (id: ".$oNode->getAttribute('id')."");
}
$oExisting->ReplaceWith($oNode);
$sFlag = 'replaced';
}
else
{
$this->appendChild($oNode);
$sFlag = 'added';
}
if (!$this->IsInDefinition())
{
$oNode->setAttribute('_alteration', $sFlag);
}
}
/**
* Modify a node and set the flags that will be used to compute the delta
* @param MFElement $oNode The node (including all subnodes) to set
*/
public function RedefineChildNode(MFElement $oNode, $sSearchId = null)
{
$oExisting = $this->FindExistingChildNode($oNode, $sSearchId);
if (!$oExisting)
{
throw new Exception("Attempting to modify a non existing node: $oNode->tagName (id: ".$oNode->getAttribute('id').")");
}
if ($oExisting->getAttribute('_alteration') == 'removed')
{
throw new Exception("Attempting to modify a deleted node: $oNode->tagName (id: ".$oNode->getAttribute('id')."");
}
$oExisting->ReplaceWith($oNode);
if (!$this->IsInDefinition())
{
$oNode->setAttribute('_alteration', 'replaced');
}
}
/**
* Replaces a node by another one, making sure that recursive nodes are preserved
* @param MFElement $oNewNode The replacement
*/
protected function ReplaceWith($oNewNode)
{
// Move the classes from the old node into the new one
foreach($this->GetNodes('class') as $oChild)
{
$oNewNode->appendChild($oChild);
}
$oParentNode = $this->parentNode;
$oParentNode->replaceChild($oNewNode, $this);
}
/**
* Remove a node and set the flags that will be used to compute the delta
*/
public function Delete()
{
$oParent = $this->parentNode;
switch ($this->getAttribute('_alteration'))
{
case 'replaced':
$sFlag = 'removed';
break;
case 'added':
$sFlag = null;
break;
case 'removed':
throw new Exception("Attempting to remove a deleted node: $this->tagName (id: ".$this->getAttribute('id')."");
default:
$sFlag = 'removed';
if ($this->IsInDefinition())
{
$sFlag = null;
break;
}
}
if ($sFlag)
{
$this->setAttribute('_alteration', $sFlag);
$this->DeleteChildren();
}
else
{
// Remove the node entirely
$this->parentNode->removeChild($this);
}
}
/**
* Merge the current node into the given container
*
* @param DOMNode $oContainer An element or a document
* @param string $sSearchId The id to consider (could be blank)
* @param bool $bMustExist Throw an exception if the node must already be found (and not marked as deleted!)
*/
public function MergeInto($oContainer, $sSearchId, $bMustExist)
{
$oTargetNode = $oContainer->FindExistingChildNode($this, $sSearchId);
if ($oTargetNode)
{
if ($oTargetNode->getAttribute('_alteration') == 'removed')
{
if ($bMustExist)
{
throw new Exception("XML datamodel loader: found mandatory node $this->tagName/$sSearchId marked as deleted in $oContainer->tagName");
}
// Beware: ImportNode(xxx, false) DOES NOT copy the node's attribute on *some* PHP versions (<5.2.17)
// So use this workaround to import a node and its attributes on *any* PHP version
$oTargetNode = $oContainer->ownerDocument->ImportNode($this->cloneNode(false), true);
$oContainer->AddChildNode($oTargetNode);
}
}
else
{
if ($bMustExist)
{
echo "Dumping parent node
\n";
$oContainer->Dump();
throw new Exception("XML datamodel loader: could not find $this->tagName/$sSearchId in $oContainer->tagName");
}
// Beware: ImportNode(xxx, false) DOES NOT copy the node's attribute on *some* PHP versions (<5.2.17)
// So use this workaround to import a node and its attributes on *any* PHP version
$oTargetNode = $oContainer->ownerDocument->ImportNode($this->cloneNode(false), true);
$oContainer->AddChildNode($oTargetNode);
}
return $oTargetNode;
}
/**
* Renames a node and set the flags that will be used to compute the delta
* @param String $sNewId The new id
*/
public function Rename($sId)
{
if (($this->getAttribute('_alteration') == 'replaced') || !$this->IsInDefinition())
{
$sOriginalId = $this->getAttribute('_old_id');
if ($sOriginalId == '')
{
$this->setAttribute('_old_id', $this->getAttribute('id'));
}
else if($sOriginalId == $sId)
{
$this->removeAttribute('_old_id');
}
}
$this->setAttribute('id', $sId);
}
/**
* Apply extensibility rules onto this node
* @param array aRules Array of rules (strings)
* @return void
*/
public function RestrictExtensibility($aRules)
{
$oRulesNode = $this->GetOptionalElement('rules');
if ($oRulesNode)
{
$aCurrentRules = $oRulesNode->GetNodeAsArrayOfItems();
$aCurrentRules = array_merge($aCurrentRules, $aRules);
$oRulesNode->SetNodeAsArrayOfItems($aCurrentRules);
}
else
{
$oNewNode = $this->ownerDocument->CreateElement('rules');
$this->appendChild($oNewNode);
$oNewNode->SetNodeAsArrayOfItems($aRules);
}
}
/**
* Read extensibility rules for this node
* @return Array of rules (strings)
*/
public function GetExtensibilityRules()
{
$aCurrentRules = array();
$oRulesNode = $this->GetOptionalElement('rules');
if ($oRulesNode)
{
$aCurrentRules = $oRulesNode->GetNodeAsArrayOfItems();
}
return $aCurrentRules;
}
}
/**
* MFDocument - formating rules for XML input/output
* @package ModelFactory
*/
class MFDocument extends DOMDocument
{
public function __construct()
{
parent::__construct('1.0', 'UTF-8');
$this->registerNodeClass('DOMElement', 'MFElement');
$this->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
$this->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
}
/**
* Overload of the standard API
*/
public function load($filename, $options = 0)
{
parent::load($filename, LIBXML_NOBLANKS);
}
/**
* Overload of the standard API
*/
public function loadXML($source, $options = 0)
{
parent::loadXML($source, LIBXML_NOBLANKS);
}
/**
* Overload the standard API
*/
public function saveXML(DOMNode $node = null, $options = 0)
{
$oRootNode = $this->firstChild;
if (!$oRootNode)
{
$oRootNode = $this->createElement('itop_design'); // make sure that the document is not empty
$oRootNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
$this->appendChild($oRootNode);
}
return parent::saveXML();
}
/**
* For debugging purposes
*/
public function Dump($bReturnRes = false)
{
$sXml = $this->saveXML();
if ($bReturnRes)
{
return $sXml;
}
else
{
echo "\n";
echo htmlentities($sXml);
echo "
\n";
}
}
/**
* Find the child node matching the given node
* @param MFElement $oRefNode The node to search for
* @param bool $sSearchId substitutes to the value of the 'id' attribute
*/
public function FindExistingChildNode(MFElement $oRefNode, $sSearchId = null)
{
return MFElement::FindNode($this, $oRefNode, $sSearchId);
}
/**
* Extracts some nodes from the DOM
* @param string $sXPath A XPath expression
* @return DOMNodeList
*/
public function GetNodes($sXPath, $oContextNode = null)
{
$oXPath = new DOMXPath($this);
if (is_null($oContextNode))
{
return $oXPath->query($sXPath);
}
else
{
return $oXPath->query($sXPath, $oContextNode);
}
}
public function GetNodeById($sXPath, $sId, $oContextNode = null)
{
$oXPath = new DOMXPath($this);
$sXPath .= "[@id='$sId' and(not(@_alteration) or @_alteration!='removed')]";
if (is_null($oContextNode))
{
return $oXPath->query($sXPath);
}
else
{
return $oXPath->query($sXPath, $oContextNode);
}
}
}