PageRenderTime 60ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/core/src/main/php/xml/Node.class.php

https://github.com/Gamepay/xp-framework
PHP | 494 lines | 234 code | 43 blank | 217 comment | 43 complexity | 9c21fb5822814324c0efb640793a52a0 MD5 | raw file
  1. <?php
  2. /* This class is part of the XP framework
  3. *
  4. * $Id$
  5. *
  6. */
  7. uses(
  8. 'xml.PCData',
  9. 'xml.CData',
  10. 'xml.XMLFormatException'
  11. );
  12. define('INDENT_DEFAULT', 0);
  13. define('INDENT_WRAPPED', 1);
  14. define('INDENT_NONE', 2);
  15. define('XML_ILLEGAL_CHARS', "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f");
  16. /**
  17. * Represents a node
  18. *
  19. * @see xp://xml.Tree#addChild
  20. * @test xp://net.xp_framework.unittest.xml.NodeTest
  21. */
  22. class Node extends Object {
  23. const
  24. XML_ILLEGAL_CHARS = XML_ILLEGAL_CHARS;
  25. public
  26. $name = '',
  27. $attribute = array(),
  28. $content = NULL,
  29. $children = array();
  30. /**
  31. * Constructor
  32. *
  33. * <code>
  34. * $n= new Node('document');
  35. * $n= new Node('text', 'Hello World');
  36. * $n= new Node('article', '', array('id' => 42));
  37. * </code>
  38. *
  39. * @param string name
  40. * @param string content default NULL
  41. * @param [:string] attribute default array() attributes
  42. * @throws lang.IllegalArgumentException
  43. */
  44. public function __construct($name, $content= NULL, $attribute= array()) {
  45. $this->name= $name;
  46. $this->attribute= $attribute;
  47. $this->setContent($content);
  48. }
  49. /**
  50. * Create a node from an array
  51. *
  52. * Usage example:
  53. * <code>
  54. * $n= Node::fromArray($array, 'elements');
  55. * </code>
  56. *
  57. * @param array arr
  58. * @param string name default 'array'
  59. * @return xml.Node
  60. */
  61. public static function fromArray($a, $name= 'array') {
  62. $n= new self($name);
  63. $sname= rtrim($name, 's');
  64. foreach (array_keys($a) as $field) {
  65. $nname= is_numeric($field) || '' == $field ? $sname : $field;
  66. if (is_array($a[$field])) {
  67. $n->addChild(self::fromArray($a[$field], $nname));
  68. } else if ($a[$field] instanceof String) {
  69. $n->addChild(new self($nname, $a[$field]));
  70. } else if (is_object($a[$field])) {
  71. $n->addChild(self::fromObject($a[$field], $nname));
  72. } else {
  73. $n->addChild(new self($nname, $a[$field]));
  74. }
  75. }
  76. return $n;
  77. }
  78. /**
  79. * Create a node from an object. Will use class name as node name
  80. * if the optional argument name is omitted.
  81. *
  82. * Usage example:
  83. * <code>
  84. * $n= Node::fromObject($object);
  85. * </code>
  86. *
  87. * @param lang.Generic obj
  88. * @param string name default NULL
  89. * @return xml.Node
  90. */
  91. public static function fromObject($obj, $name= NULL) {
  92. if (!method_exists($obj, '__sleep')) {
  93. $vars= get_object_vars($obj);
  94. } else {
  95. $vars= array();
  96. foreach ($obj->__sleep() as $var) $vars[$var]= $obj->{$var};
  97. }
  98. if (NULL !== $name) return self::fromArray($vars, $name);
  99. $class= get_class($obj);
  100. return self::fromArray($vars, (FALSE !== ($p= strrpos($class, '::'))) ? substr($class, $p+ 2): $class);
  101. }
  102. /**
  103. * Set Name
  104. *
  105. * @param string name
  106. */
  107. public function setName($name) {
  108. $this->name= $name;
  109. }
  110. /**
  111. * Get Name
  112. *
  113. * @return string
  114. */
  115. public function getName() {
  116. return $this->name;
  117. }
  118. /**
  119. * Set content
  120. *
  121. * @param string content
  122. * @throws xml.XMLFormatException in case content contains illegal characters
  123. */
  124. public function setContent($content) {
  125. // Scan the given string for illegal characters.
  126. if (is_string($content)) {
  127. if (strlen($content) > ($p= strcspn($content, XML_ILLEGAL_CHARS))) {
  128. throw new XMLFormatException(
  129. 'Content contains illegal character at position '.$p. ' / chr('.ord($content{$p}).')'
  130. );
  131. }
  132. }
  133. $this->content= $content;
  134. }
  135. /**
  136. * Get content (all CDATA)
  137. *
  138. * @return string content
  139. */
  140. public function getContent() {
  141. return $this->content;
  142. }
  143. /**
  144. * Set an attribute
  145. *
  146. * @param string name
  147. * @param string value
  148. */
  149. public function setAttribute($name, $value) {
  150. $this->attribute[$name]= $value;
  151. }
  152. /**
  153. * Sets all attributes
  154. *
  155. * @param <string,string>[] attributes
  156. */
  157. public function setAttributes($attrs) {
  158. $this->attribute= $attrs;
  159. }
  160. /**
  161. * Retrieve an attribute by its name. Returns the default value if the
  162. * attribute is non-existant
  163. *
  164. * @param string name
  165. * @param var default default NULL
  166. * @return string
  167. */
  168. public function getAttribute($name, $default= NULL) {
  169. return isset($this->attribute[$name]) ? $this->attribute[$name] : $default;
  170. }
  171. /**
  172. * Retrieve all attributes
  173. *
  174. * @return <string, string>[] attributes
  175. */
  176. public function getAttributes() {
  177. return $this->attribute;
  178. }
  179. /**
  180. * Checks whether a specific attribute is existant
  181. *
  182. * @param string name
  183. * @return bool
  184. */
  185. public function hasAttribute($name) {
  186. return isset($this->attribute[$name]);
  187. }
  188. /**
  189. * Retrieve XML representation
  190. *
  191. * Setting indent to 0 (INDENT_DEFAULT) yields this result:
  192. * <pre>
  193. * <item>
  194. * <title>Website created</title>
  195. * <link/>
  196. * <description>The first version of the XP web site is online</description>
  197. * <dc:date>2002-12-27T13:10:00</dc:date>
  198. * </item>
  199. * </pre>
  200. *
  201. * Setting indent to 1 (INDENT_WRAPPED) yields this result:
  202. * <pre>
  203. * <item>
  204. * <title>
  205. * Website created
  206. * </title>
  207. * <link/>
  208. * <description>
  209. * The first version of the XP web site is online
  210. * </description>
  211. * <dc:date>
  212. * 2002-12-27T13:10:00
  213. * </dc:date>
  214. * </item>
  215. * </pre>
  216. *
  217. * Setting indent to 2 (INDENT_NONE) yields this result (wrapped for readability,
  218. * returned XML is on one line):
  219. * <pre>
  220. * <item><title>Website created</title><link></link><description>The
  221. * first version of the XP web site is online</description><dc:date>
  222. * 2002-12-27T13:10:00</dc:date></item>
  223. * </pre>
  224. *
  225. * @param int indent default INDENT_WRAPPED
  226. * @param string encoding defaults to XP default encoding
  227. * @param string inset default ''
  228. * @param string encoding of tree nodes default 'iso-8859-1'
  229. * @return string XML
  230. */
  231. public function getSource($indent= INDENT_WRAPPED, $encoding= xp::ENCODING, $inset= '',$tree_encoding= 'iso-8859-1') {
  232. $xml= $inset.'<'.$this->name;
  233. $conv= $tree_encoding != $encoding;
  234. if ('string' == ($type= gettype($this->content))) {
  235. $content= $conv
  236. ? iconv($tree_encoding, $encoding, htmlspecialchars($this->content, ENT_COMPAT, $tree_encoding))
  237. : htmlspecialchars($this->content, ENT_COMPAT, $tree_encoding)
  238. ;
  239. } else if ('float' == $type) {
  240. $content= ($this->content - floor($this->content) == 0)
  241. ? number_format($this->content, 0, NULL, NULL)
  242. : $this->content
  243. ;
  244. } else if ($this->content instanceof PCData) {
  245. $content= $conv
  246. ? iconv($tree_encoding, $encoding, $this->content->pcdata)
  247. : $this->content->pcdata
  248. ;
  249. } else if ($this->content instanceof CData) {
  250. $content= '<![CDATA['.str_replace(']]>', ']]]]><![CDATA[>', $conv
  251. ? iconv($tree_encoding, $encoding, $this->content->cdata)
  252. : $this->content->cdata
  253. ).']]>';
  254. } else if ($this->content instanceof String) {
  255. $content= htmlspecialchars($this->content->getBytes($encoding), ENT_COMPAT, $encoding);
  256. } else {
  257. $content= $this->content;
  258. }
  259. if (INDENT_NONE === $indent) {
  260. foreach ($this->attribute as $key => $value) {
  261. $xml.= ' '.$key.'="'.htmlspecialchars(
  262. $conv ? iconv($tree_encoding, $encoding, $value) : $value,
  263. ENT_COMPAT,
  264. $tree_encoding
  265. ).'"';
  266. }
  267. $xml.= '>'.$content;
  268. foreach ($this->children as $child) {
  269. $xml.= $child->getSource($indent, $encoding, $inset, $tree_encoding);
  270. }
  271. return $xml.'</'.$this->name.'>';
  272. } else {
  273. if ($this->attribute) {
  274. $sep= (sizeof($this->attribute) < 3) ? '' : "\n".$inset;
  275. foreach ($this->attribute as $key => $value) {
  276. $xml.= $sep.' '.$key.'="'.htmlspecialchars(
  277. $conv ? iconv($tree_encoding, $encoding, $value) : $value,
  278. ENT_COMPAT,
  279. $tree_encoding
  280. ).'"';
  281. }
  282. $xml.= $sep;
  283. }
  284. // No content and no children => close tag
  285. if (0 == strlen($content)) {
  286. if (!$this->children) return $xml."/>\n";
  287. $xml.= '>';
  288. } else {
  289. $xml.= '>'.($indent ? "\n ".$inset.$content : trim($content));
  290. }
  291. if ($this->children) {
  292. $xml.= ($indent ? '' : $inset)."\n";
  293. foreach ($this->children as $child) {
  294. $xml.= $child->getSource($indent, $encoding, $inset.' ', $tree_encoding);
  295. }
  296. $xml= ($indent ? substr($xml, 0, -1) : $xml).$inset;
  297. }
  298. return $xml.($indent ? "\n".$inset : '').'</'.$this->name.">\n";
  299. }
  300. }
  301. /**
  302. * Add a child node
  303. *
  304. * @param xml.Node child
  305. * @return xml.Node added child
  306. * @throws lang.IllegalArgumentException in case the given argument is not a Node
  307. */
  308. public function addChild(Node $child) {
  309. $this->children[]= $child;
  310. return $child;
  311. }
  312. /**
  313. * Add a child node and return this node
  314. *
  315. * @param xml.Node child
  316. * @return xml.Node this
  317. * @throws lang.IllegalArgumentException in case the given argument is not a Node
  318. */
  319. public function withChild(Node $child) {
  320. $this->addChild($child);
  321. return $this;
  322. }
  323. /**
  324. * Set children to given list of children
  325. *
  326. * @param xml.Node[] children
  327. */
  328. public function setChildren(array $children) {
  329. $this->children= array();
  330. foreach ($children as $child) {
  331. $this->addChild($child);
  332. }
  333. }
  334. /**
  335. * Retrieve node children
  336. *
  337. * @return xml.Node[] children
  338. */
  339. public function getChildren() {
  340. return $this->children;
  341. }
  342. /**
  343. * Clear node children
  344. *
  345. */
  346. public function clearChildren() {
  347. $this->setChildren(array());
  348. }
  349. /**
  350. * Retrieve number of children
  351. *
  352. * @return int
  353. */
  354. public function numChildren() {
  355. return sizeof($this->children);
  356. }
  357. /**
  358. * Determine whether node has node children
  359. *
  360. * @return bool
  361. */
  362. public function hasChildren() {
  363. return 0 < sizeof($this->children);
  364. }
  365. /**
  366. * Retrieve nth node child
  367. *
  368. * @param int pos
  369. * @return xml.Node
  370. * @throws lang.ElementNotFoundException if array index out of bounds
  371. */
  372. public function nodeAt($pos) {
  373. if (!isset($this->children[$pos])) {
  374. throw new ElementNotFoundException('Cannot access node at position '.$pos);
  375. }
  376. return $this->children[$pos];
  377. }
  378. /**
  379. * Returns whether another object is equal to this node
  380. *
  381. * @param lang.Generic cmp
  382. * @return bool
  383. */
  384. public function equals($cmp) {
  385. return $cmp instanceof self && $this->toString() === $cmp->toString();
  386. }
  387. /**
  388. * Creates a string representation of this object
  389. *
  390. * @return string
  391. */
  392. public function toString() {
  393. $a= '';
  394. foreach ($this->attribute as $name => $value) {
  395. $a.= ' @'.$name.'= '.xp::stringOf($value);
  396. }
  397. $s= $this->getClassName().'('.$this->name.$a.') {';
  398. if (!$this->children) {
  399. $s.= NULL === $this->content ? ' ' : ' '.xp::stringOf($this->content).' ';
  400. } else {
  401. $s.= NULL === $this->content ? "\n" : "\n ".xp::stringOf($this->content)."\n";
  402. foreach ($this->children as $child) {
  403. $s.= ' '.str_replace("\n", "\n ", xp::stringOf($child))."\n";
  404. }
  405. }
  406. return $s.'}';
  407. }
  408. /**
  409. * remove a child node by index
  410. *
  411. * @param xml.Node child
  412. */
  413. private function removeChild($index) {
  414. try {
  415. unset($this->children[$index]);
  416. $this->children= array_values($this->children);
  417. return TRUE;
  418. } catch (Exception $exc) {
  419. return FALSE;
  420. }
  421. }
  422. /**
  423. * remove a child node by node
  424. *
  425. * @param xml.Node child
  426. */
  427. public function removeChildByNode(Node $node) {
  428. $index= array_search($node, $this->children);
  429. if (!$index) {
  430. throw new Exception("Node not found");
  431. }
  432. $this->removeChild($index);
  433. }
  434. /**
  435. * remove a child node by tag
  436. *
  437. * @param string tag
  438. */
  439. public function removeChildByTag($tag) {
  440. $found= FALSE;
  441. foreach ($this->children as $key => $child) {
  442. if ($child->getName() == $tag) {
  443. $found= $this->removeChild($key);
  444. }
  445. }
  446. if (!$found) {
  447. throw new Exception("Node '.$tag.' not found");
  448. }
  449. }
  450. }
  451. ?>