1 /* Copyright 2002-2005 Elliotte Rusty Harold 2 3 This library is free software; you can redistribute it and/or modify 4 it under the terms of version 2.1 of the GNU Lesser General Public 5 License as published by the Free Software Foundation. 6 7 This library is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 GNU Lesser General Public License for more details. 11 12 You should have received a copy of the GNU Lesser General Public 13 License along with this library; if not, write to the 14 Free Software Foundation, Inc., 59 Temple Place, Suite 330, 15 Boston, MA 02111-1307 USA 16 17 You can contact Elliotte Rusty Harold by sending e-mail to 18 elharo@ibiblio.org. Please include the word "XOM" in the 19 subject line. The XOM home page is located at http://www.xom.nu/ 20 */ 21 22 package nu.xom; 23 24 25 /** 26 * 27 * <p> 28 * The generic superclass for nodes that have children. 29 * Not counting subclasses, there are exactly two such classes in XOM: 30 * </p> 31 * 32 * <ul> 33 * <li><code>Document</code></li> 34 * <li><code>Element</code></li> 35 * </ul> 36 * 37 * <p> 38 * This class provides methods to add and remove child nodes. 39 * </p> 40 * 41 * 42 * @author Elliotte Rusty Harold 43 * @version 1.2.3 44 * 45 */ 46 public abstract class ParentNode extends Node { 47 48 Node[] children; 49 int childCount = 0; 50 String actualBaseURI; 51 52 /** 53 * <p> 54 * Creates a new <code>ParentNode</code> object. 55 * Can only be invoked by other members of 56 * the <code>nu.xom</code> package. 57 * </p> 58 * 59 */ ParentNode()60 ParentNode() {} 61 62 63 /** 64 * <p> 65 * Returns the number of child nodes this node contains. 66 * This is always greater than or equal to 0. 67 * </p> 68 * 69 * @return the number of children of this node 70 */ getChildCount()71 public int getChildCount() { 72 return childCount; 73 } 74 75 76 /** 77 * <p> 78 * Inserts a child node at the specified position. 79 * The child node previously at that position (if any) 80 * and all subsequent child nodes are moved up by one. 81 * That is, when inserting a node at 2, the old node at 2 82 * is moved to 3, the old child at 3 is moved to 4, and so 83 * forth. Inserting at position 0 makes the child the first 84 * child of this node. Inserting at the position 85 * <code>getChildCount()</code> makes the child the 86 * last child of the node. 87 * </p> 88 * 89 * <p> 90 * All the other methods that add a node to the tree ultimately 91 * invoke this method. 92 * </p> 93 * 94 * @param position where to insert the child 95 * @param child the node to insert 96 * 97 * @throws IllegalAddException if this node cannot have a child of 98 * the argument's type 99 * @throws MultipleParentException if <code>child</code> already 100 * has a parent 101 * @throws NullPointerException if <code>child</code> is null 102 * @throws IndexOutOfBoundsException if the position is negative or 103 * greater than the number of children of this node 104 */ insertChild(Node child, int position)105 public void insertChild(Node child, int position) { 106 _insertChild(child, position); 107 } 108 109 110 // because this method is called from Document constructor and 111 // constructors should not call overridable methods _insertChild(Node child, int position)112 final void _insertChild(Node child, int position) { 113 insertionAllowed(child, position); 114 fastInsertChild(child, position); 115 } 116 117 fastInsertChild(Node child, int position)118 void fastInsertChild(Node child, int position) { 119 if (position > childCount) { 120 throw new IndexOutOfBoundsException("Inserted node at position " + position + " after children"); 121 } 122 checkCapacity(childCount+1); 123 if (position < childCount) { 124 // make space in the middle for the new child; otherwise just add it at the end 125 System.arraycopy(children, position, children, position+1, childCount-position); 126 } 127 children[position] = child; 128 childCount++; 129 child.setParent(this); 130 } 131 132 checkCapacity(int position)133 private void checkCapacity(int position) { 134 135 if (children == null) { 136 children = new Node[1]; 137 } 138 else if (position >= children.length) { 139 Node[] data = new Node[children.length * 2]; 140 System.arraycopy(children, 0, data, 0, children.length); 141 this.children = data; 142 } 143 144 } 145 146 insertionAllowed(Node child, int position)147 abstract void insertionAllowed(Node child, int position); 148 149 150 /** 151 * <p> 152 * Appends a node to the children of this node. 153 * </p> 154 * 155 * @param child node to append to this node 156 * 157 * @throws IllegalAddException if this node cannot have children 158 * of this type 159 * @throws MultipleParentException if child already has a parent 160 * @throws NullPointerException if <code>child</code> is null 161 * 162 */ appendChild(Node child)163 public void appendChild(Node child) { 164 insertChild(child, childCount); 165 } 166 167 168 /** 169 *<p> 170 * Returns the child of this node at the specified position. 171 * Indexes begin at 0 and count up to one less than the number 172 * of children in this node. 173 * </p> 174 * 175 * @param position index of the node to return 176 * 177 * @return the node at the requested position 178 * 179 * @throws IndexOutOfBoundsException if the index is negative or 180 * greater than or equal to the number of children of this node 181 */ getChild(int position)182 public Node getChild(int position) { 183 184 if (children == null) { 185 throw new IndexOutOfBoundsException( 186 "This node has no children" 187 ); 188 } 189 return children[position]; 190 191 } 192 193 194 // private int lastPosition = -1; 195 196 /** 197 *<p> 198 * Returns the position of a node within the children of this 199 * node. This is a number between 0 and one less than the number of 200 * children of this node. It returns -1 if <code>child</code> 201 * does not have this node as a parent. 202 * </p> 203 * 204 * <p> 205 * This method does a linear search through the node's children. 206 * On average, it executes in O(N) where N is the number of 207 * children of the node. 208 * </p> 209 * 210 * @param child the node whose position is desired 211 * 212 * @return the position of the argument node among 213 * the children of this node 214 */ indexOf(Node child)215 public int indexOf(Node child) { 216 217 if (children == null) return -1; 218 219 // Programs tend to iterate through in order so we store the 220 // last index returned and check the one immediately after it 221 // first; before searching the list from the beginning. 222 /* lastPosition++; 223 if (lastPosition != children.size()) { 224 if (child == children.get(lastPosition)) { 225 return lastPosition; 226 } 227 else lastPosition = -1; 228 } 229 lastPosition = children.indexOf(child); 230 return lastPosition; */ 231 for (int i = 0; i < childCount; i++) { 232 if (child == children[i]) return i; 233 } 234 return -1; 235 236 } 237 238 239 /** 240 * <p> 241 * Removes the child of this node at the specified position. 242 * Indexes begin at 0 and count up to one less than the number 243 * of children in this node. 244 * </p> 245 * 246 * @param position index of the node to remove 247 * 248 * @return the node which was removed 249 * 250 * @throws IndexOutOfBoundsException if the index is negative or 251 * greater than or equal to the number of children of this node 252 */ removeChild(int position)253 public Node removeChild(int position) { 254 255 if (children == null) { 256 throw new IndexOutOfBoundsException( 257 "This node has no children" 258 ); 259 } 260 Node removed = children[position]; 261 // fill in actual base URI 262 // This way does add base URIs to elements created in memory 263 // XXX but this is a HotSpot when building; we need a fastRemoveChild 264 if (removed.isElement()) fillInBaseURI((Element) removed); 265 266 int toCopy = childCount-position-1; 267 if (toCopy > 0) { 268 System.arraycopy(children, position+1, children, position, toCopy); 269 } 270 childCount--; 271 children[childCount] = null; 272 removed.setParent(null); 273 274 return removed; 275 276 } 277 278 fillInBaseURI(Element removed)279 void fillInBaseURI(Element removed) { 280 281 ParentNode parent = removed; 282 String actualBaseURI = ""; 283 while (parent != null && actualBaseURI.equals("")) { 284 actualBaseURI = parent.getActualBaseURI(); 285 parent = parent.getParent(); 286 } 287 removed.setActualBaseURI(actualBaseURI); 288 289 } 290 291 292 /** 293 * <p> 294 * Removes the specified child of this node. 295 * </p> 296 * 297 * @param child child node to remove 298 * 299 * @return the node which was removed 300 * 301 * @throws NoSuchChildException if <code>child</code> is 302 * not in fact a child of this node 303 */ removeChild(Node child)304 public Node removeChild(Node child) { 305 306 if (children == null) { 307 throw new NoSuchChildException( 308 "Child does not belong to this node" 309 ); 310 } 311 // This next line is a hot spot 312 int position = indexOf(child); 313 if (position == -1) { 314 throw new NoSuchChildException( 315 "Child does not belong to this node" 316 ); 317 } 318 if (child.isElement()) fillInBaseURI((Element) child); 319 removeChild(position); 320 321 child.setParent(null); 322 323 return child; 324 325 } 326 327 328 /** 329 * <p> 330 * Replaces an existing child with a new child node. 331 * If <code>oldChild</code> is not a child of this node, 332 * then a <code>NoSuchChildException</code> is thrown. 333 * </p> 334 * 335 * @param oldChild the node removed from the tree 336 * @param newChild the node inserted into the tree 337 * 338 * @throws MultipleParentException if <code>newChild</code> already 339 * has a parent 340 * @throws NoSuchChildException if <code>oldChild</code> 341 * is not a child of this node 342 * @throws NullPointerException if either argument is null 343 * @throws IllegalAddException if this node cannot have children 344 * of the type of <code>newChild</code> 345 */ replaceChild(Node oldChild, Node newChild)346 public void replaceChild(Node oldChild, Node newChild) { 347 348 if (oldChild == null) { 349 throw new NullPointerException( 350 "Tried to replace null child" 351 ); 352 } 353 if (newChild == null) { 354 throw new NullPointerException( 355 "Tried to replace child with null" 356 ); 357 } 358 if (children == null) { 359 throw new NoSuchChildException( 360 "Reference node is not a child of this node." 361 ); 362 } 363 int position = indexOf(oldChild); 364 if (position == -1) { 365 throw new NoSuchChildException( 366 "Reference node is not a child of this node." 367 ); 368 } 369 370 if (oldChild == newChild) return; 371 372 insertionAllowed(newChild, position); 373 removeChild(position); 374 insertChild(newChild, position); 375 376 } 377 378 379 /** 380 * 381 * <p> 382 * Sets the URI against which relative URIs in this node will be 383 * resolved. Generally, it's only necessary to set this property if 384 * it's different from a node's parent's base URI, as it may 385 * be in a document assembled from multiple entities 386 * or by XInclude. 387 * </p> 388 * 389 * <p> 390 * Relative URIs are not allowed here. Base URIs must be absolute. 391 * However, the base URI may be set to null or the empty string 392 * to indicate that the node has no explicit base URI. In this 393 * case, it inherits the base URI of its parent node, if any. 394 * </p> 395 * 396 * <p> 397 * URIs with fragment identifiers are also not allowed. The value 398 * passed to this method must be a pure URI, not a URI reference. 399 * </p> 400 * 401 * <p> 402 * You can also add an <code>xml:base</code> attribute to 403 * an element in the same way you'd add any other namespaced 404 * attribute to an element. If an element's base URI 405 * conflicts with its <code>xml:base</code> attribute, 406 * then the value found in the <code>xml:base</code> attribute 407 * is used. 408 * </p> 409 * 410 * <p> 411 * If the base URI is null or the empty string and there is 412 * no <code>xml:base</code> attribute, then the base URI is 413 * determined by the nearest ancestor node which does have a 414 * base URI. Moving such a node from one location to another 415 * can change its base URI. 416 * </p> 417 * 418 * @param URI the new base URI for this node 419 * 420 * @throws MalformedURIException if <code>URI</code> is 421 * not a legal RFC 3986 absolute URI 422 */ setBaseURI(String URI)423 public abstract void setBaseURI(String URI); 424 425 getActualBaseURI()426 String getActualBaseURI() { 427 if (actualBaseURI == null) return ""; 428 return actualBaseURI; 429 } 430 431 setActualBaseURI(String uri)432 void setActualBaseURI(String uri) { 433 if (uri == null) uri = ""; 434 if (!"".equals(uri)) Verifier.checkAbsoluteURI(uri); 435 actualBaseURI = uri; 436 } 437 438 findActualBaseURI()439 final String findActualBaseURI() { 440 441 ParentNode current = this; 442 while (true) { 443 String actualBase = current.getActualBaseURI(); 444 ParentNode parent = current.getParent(); 445 446 if (parent == null) return actualBase; 447 448 if ("".equals(actualBase)) { 449 current = parent; 450 continue; 451 } 452 453 // The parent is loaded from a different entity. 454 // Therefore just return the actual base. 455 return actualBase; 456 } 457 458 } 459 460 461 } 462