1 /* 2 * Copyright (C) 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.clearsilver.jsilver.data; 18 19 20 import java.io.IOException; 21 import java.util.Collections; 22 import java.util.HashMap; 23 import java.util.Iterator; 24 import java.util.Map; 25 import java.util.NoSuchElementException; 26 27 /** 28 * Represents a hierarchical data set of primitives. 29 * 30 * This is the JSilver equivalent to ClearSilver's HDF object. 31 * 32 * This class has no synchronization logic. Follow the same thread-safety semantics as you would for 33 * a java.util.ArrayList (i.e. concurrent reads allowed, but ensure you have exclusive access when 34 * modifying - should not read whilst another thread writes). 35 */ 36 public class NestedMapData extends AbstractData { 37 38 /** 39 * Number of children a node can have before we bother allocating a HashMap. We currently allocate 40 * the HashMap on the 5th child. 41 */ 42 private static final int CHILD_MAP_THRESHOLD = 4; 43 44 private String name; 45 private NestedMapData parent; 46 private final NestedMapData root; 47 48 // Lazily intitialized after CHILD_MAP_THRESHOLD is hit. 49 private Map<String, NestedMapData> children = null; 50 // Number of children 51 private int childCount = 0; 52 // First child (first sibling of children) 53 private NestedMapData firstChild = null; 54 // Last child (last sibling of children) 55 private NestedMapData lastChild = null; 56 57 /** 58 * Single object returned by getChildren(). Constructs ChildrenIterator objects. 59 */ 60 private Iterable<NestedMapData> iterableChildren = null; 61 62 // Holds the attributes for this HDF node. 63 private Map<String, String> attributeList = null; 64 65 private String value = null; 66 private NestedMapData symLink = this; 67 68 // Doubly linked list of siblings. 69 private NestedMapData prevSibling = null; 70 private NestedMapData nextSibling = null; 71 72 public NestedMapData() { 73 name = null; 74 parent = null; 75 root = this; 76 } 77 78 protected NestedMapData(String name, NestedMapData parent, NestedMapData root) { 79 this.name = name; 80 this.parent = parent; 81 this.root = root; 82 } 83 84 // ******************* Node creation and removal ******************* 85 // Must be kept in sync. 86 87 /** 88 * Creates a child of this node. 89 * 90 * @param chunk name to give the new child node. 91 * @return the NestedMapData object corresponding to the new node. 92 */ 93 protected NestedMapData createChildNode(String chunk) { 94 NestedMapData sym = followSymLinkToTheBitterEnd(); 95 NestedMapData data = new NestedMapData(chunk, sym, sym.root); 96 97 // If the parent node's child count is 5 or more and it does not have a 98 // children Hashmap, initialize it now. 99 if (sym.children == null && sym.childCount >= CHILD_MAP_THRESHOLD) { 100 sym.children = new HashMap<String, NestedMapData>(); 101 // Copy in existing children. 102 NestedMapData curr = sym.firstChild; 103 while (curr != null) { 104 sym.children.put(curr.getName(), curr); 105 curr = curr.nextSibling; 106 } 107 } 108 // If the parent node has a children map, add the new child node to it. 109 if (sym.children != null) { 110 sym.children.put(chunk, data); 111 } 112 113 data.prevSibling = sym.lastChild; 114 if (sym.lastChild != null) { 115 // Update previous lastChild to point to new child. 116 sym.lastChild.nextSibling = data; 117 } else { 118 // There were no previous children so this is the first. 119 sym.firstChild = data; 120 } 121 sym.lastChild = data; 122 123 sym.childCount++; 124 125 return data; 126 } 127 128 private void severNode() { 129 if (parent == null) { 130 return; 131 } 132 if (parent.children != null) { 133 parent.children.remove(name); 134 } 135 if (prevSibling != null) { 136 prevSibling.nextSibling = nextSibling; 137 } else { 138 parent.firstChild = nextSibling; 139 } 140 if (nextSibling != null) { 141 nextSibling.prevSibling = prevSibling; 142 } else { 143 parent.lastChild = prevSibling; 144 } 145 parent.childCount--; 146 147 // Need to cleal the parent pointer or else if someone has a direct reference to this node 148 // they will get very strange results. 149 parent = null; 150 } 151 152 // ******************* Node data ******************* 153 154 /** 155 * Returns the name of this HDF node. The root node has no name, so calling this on the root node 156 * will return null. 157 */ 158 public String getName() { 159 return name; 160 } 161 162 /** 163 * Recursive method that builds the full path to this node via its parent links into the given 164 * StringBuilder. 165 */ 166 private void getPathName(StringBuilder sb) { 167 if (parent != null && parent != root) { 168 parent.getPathName(sb); 169 sb.append("."); 170 } 171 String name = getName(); 172 if (name != null) { 173 sb.append(name); 174 } 175 } 176 177 /** 178 * Returns the full path to this node via its parent links. 179 */ 180 public String getFullPath() { 181 StringBuilder sb = new StringBuilder(); 182 getPathName(sb); 183 return sb.toString(); 184 } 185 186 /** 187 * Returns the value of this HDF node, or null if this node has no value. Every node in the tree 188 * can have a value, a child, and a next peer. 189 */ 190 public String getValue() { 191 return followSymLinkToTheBitterEnd().value; 192 } 193 194 /** 195 * Set the value of this node. Any symlink that may have been set for this node will be replaced. 196 */ 197 public void setValue(String value) { 198 // Clearsilver behaviour is to replace any symlink that may already exist 199 // here with the new value, rather than following the symlink. 200 this.symLink = this; 201 this.value = value; 202 } 203 204 // ******************* Attributes ******************* 205 206 // We don't expect attributes to be heavily used. They are not used for template parsing. 207 208 public void setAttribute(String key, String value) { 209 if (key == null) { 210 throw new NullPointerException("Attribute name cannot be null."); 211 } 212 if (attributeList == null) { 213 // Do we need to worry about synchronization? 214 attributeList = new HashMap<String, String>(); 215 } 216 if (value == null) { 217 attributeList.remove(key); 218 } else { 219 attributeList.put(key, value); 220 } 221 } 222 223 public String getAttribute(String key) { 224 return attributeList == null ? null : attributeList.get(key); 225 } 226 227 public boolean hasAttribute(String key) { 228 return attributeList != null && attributeList.containsKey(key); 229 } 230 231 public int getAttributeCount() { 232 return attributeList == null ? 0 : attributeList.size(); 233 } 234 235 public Iterable<Map.Entry<String, String>> getAttributes() { 236 if (attributeList == null) { 237 return Collections.emptySet(); 238 } 239 return attributeList.entrySet(); 240 } 241 242 // ******************* Children ******************* 243 244 /** 245 * Return the root of the tree where the current node lies. If the current node is the root, 246 * return this. 247 */ 248 public Data getRoot() { 249 return root; 250 } 251 252 /** 253 * Get the parent node. 254 */ 255 public Data getParent() { 256 return parent; 257 } 258 259 /** 260 * Is this the first of its siblings? 261 */ 262 @Override 263 public boolean isFirstSibling() { 264 return prevSibling == null; 265 } 266 267 /** 268 * Is this the last of its siblings? 269 */ 270 @Override 271 public boolean isLastSibling() { 272 return nextSibling == null; 273 } 274 275 public Data getNextSibling() { 276 return nextSibling; 277 } 278 279 /** 280 * Returns number of child nodes. 281 */ 282 @Override 283 public int getChildCount() { 284 return followSymLinkToTheBitterEnd().childCount; 285 } 286 287 /** 288 * Returns children of this node. 289 */ 290 @Override 291 public Iterable<? extends Data> getChildren() { 292 if (iterableChildren == null) { 293 iterableChildren = new IterableChildren(); 294 } 295 return iterableChildren; 296 } 297 298 /** 299 * Retrieves the object that is the root of the subtree at hdfpath, returning null if the subtree 300 * doesn't exist 301 */ 302 public NestedMapData getChild(String path) { 303 NestedMapData current = this; 304 for (int lastDot = 0, nextDot = 0; nextDot != -1 && current != null; lastDot = nextDot + 1) { 305 nextDot = path.indexOf('.', lastDot); 306 String chunk = nextDot == -1 ? path.substring(lastDot) : path.substring(lastDot, nextDot); 307 current = current.followSymLinkToTheBitterEnd().getChildNode(chunk); 308 } 309 return current; 310 } 311 312 /** 313 * Retrieves the HDF object that is the root of the subtree at hdfpath, create the subtree if it 314 * doesn't exist 315 */ 316 public NestedMapData createChild(String path) { 317 NestedMapData current = this; 318 for (int lastDot = 0, nextDot = 0; nextDot != -1; lastDot = nextDot + 1) { 319 nextDot = path.indexOf('.', lastDot); 320 String chunk = nextDot == -1 ? path.substring(lastDot) : path.substring(lastDot, nextDot); 321 NestedMapData currentSymLink = current.followSymLinkToTheBitterEnd(); 322 current = currentSymLink.getChildNode(chunk); 323 if (current == null) { 324 current = currentSymLink.createChildNode(chunk); 325 } 326 } 327 return current; 328 } 329 330 /** 331 * Non-recursive method that only returns a Data node if this node has a child whose name matches 332 * the specified name. 333 * 334 * @param name String containing the name of the child to look for. 335 * @return a Data node that is the child of this node and named 'name', otherwise {@code null}. 336 */ 337 private NestedMapData getChildNode(String name) { 338 NestedMapData sym = followSymLinkToTheBitterEnd(); 339 if (sym.getChildCount() == 0) { 340 // No children. Just return null. 341 return null; 342 } 343 if (sym.children != null) { 344 // children map exists. Look it up there. 345 return sym.children.get(name); 346 } 347 // Iterate through linked list of children and look for a name match. 348 NestedMapData curr = sym.firstChild; 349 while (curr != null) { 350 if (curr.getName().equals(name)) { 351 return curr; 352 } 353 curr = curr.nextSibling; 354 } 355 return null; 356 } 357 358 /** 359 * Remove the specified subtree. 360 */ 361 public void removeTree(String path) { 362 NestedMapData removed = getChild(path); 363 if (removed != null) { 364 removed.severNode(); 365 } 366 } 367 368 private NestedMapData followSymLinkToTheBitterEnd() { 369 NestedMapData current; 370 for (current = this; current.symLink != current; current = current.symLink); 371 return current; 372 } 373 374 // ******************* Symbolic links ******************* 375 376 /** 377 * Set the source node to be a symbolic link to the destination. 378 */ 379 public void setSymlink(String sourcePath, String destinationPath) { 380 setSymlink(sourcePath, createChild(destinationPath)); 381 } 382 383 /** 384 * Set the source node to be a symbolic link to the destination. 385 */ 386 public void setSymlink(String sourcePath, Data destination) { 387 createChild(sourcePath).setSymlink(destination); 388 } 389 390 /** 391 * Set this node to be a symbolic link to another node. 392 */ 393 public void setSymlink(Data symLink) { 394 if (symLink instanceof NestedMapData) { 395 this.symLink = (NestedMapData) symLink; 396 } else { 397 String errorMessage = 398 "Cannot set symlink of incompatible Data type: " + symLink.getClass().getName(); 399 if (symLink instanceof ChainedData) { 400 errorMessage += 401 "\nOther type is ChainedData indicating there are " 402 + "multiple valid Data nodes for the path: " + symLink.getFullPath(); 403 } 404 throw new IllegalArgumentException(errorMessage); 405 } 406 } 407 408 /** 409 * Retrieve the symbolic link this node points to. Will return reference to self if not a symlink. 410 */ 411 public Data getSymlink() { 412 return symLink; 413 } 414 415 // ************************ Copy ************************* 416 417 public void copy(String toPath, Data from) { 418 if (toPath == null) { 419 throw new NullPointerException("Invalid copy destination path"); 420 } 421 if (from == null) { 422 // Is this a nop or should we throw an error? 423 return; 424 } 425 Data to = createChild(toPath); 426 to.copy(from); 427 } 428 429 public void copy(Data from) { 430 if (from == null) { 431 // Is this a nop or should we throw an error? 432 return; 433 } 434 // Clear any existing symlink. 435 this.symLink = this; 436 437 // Clear any existing attributes and copy the ones from the source. 438 if (this.attributeList != null) { 439 this.attributeList.clear(); 440 } 441 for (Map.Entry<String, String> attribute : from.getAttributes()) { 442 setAttribute(attribute.getKey(), attribute.getValue()); 443 } 444 445 // If the source node was a symlink, just copy the link over and we're done. 446 if (from.getSymlink() != from) { 447 setSymlink(from.getSymlink()); 448 return; 449 } 450 451 // Copy over the node's value. 452 setValue(from.getValue()); 453 454 // For each source child, create a new child with the same name and recurse. 455 for (Data fromChild : from.getChildren()) { 456 Data toChild = createChild(fromChild.getName()); 457 toChild.copy(fromChild); 458 } 459 } 460 461 /** 462 * Write out the String representation of this HDF node. 463 */ 464 public void write(Appendable out, int indent) throws IOException { 465 if (symLink != this) { 466 indent(out, indent); 467 writeNameAttrs(out); 468 out.append(" : ").append(symLink.getFullPath()).append('\n'); 469 return; 470 } 471 if (getValue() != null) { 472 indent(out, indent); 473 writeNameAttrs(out); 474 if (getValue().contains("\n")) { 475 writeMultiline(out); 476 } else { 477 out.append(" = ").append(getValue()).append('\n'); 478 } 479 } 480 if (getChildCount() > 0) { 481 int childIndent = indent; 482 if (this != root) { 483 indent(out, indent); 484 writeNameAttrs(out); 485 out.append(" {\n"); 486 childIndent++; 487 } 488 for (Data child : getChildren()) { 489 child.write(out, childIndent); 490 } 491 if (this != root) { 492 indent(out, indent); 493 out.append("}\n"); 494 } 495 } 496 } 497 498 /** 499 * Here we optimize the structure for long-term use. We call intern() on all Strings to reduce the 500 * copies of the same string that appear 501 */ 502 @Override 503 public void optimize() { 504 name = name == null ? null : name.intern(); 505 value = value == null ? null : value.intern(); 506 if (attributeList != null) { 507 Map<String, String> newList = new HashMap<String, String>(attributeList.size()); 508 for (Map.Entry<String, String> entry : attributeList.entrySet()) { 509 String key = entry.getKey(); 510 String value = entry.getValue(); 511 key = key == null ? null : key.intern(); 512 value = value == null ? null : value.intern(); 513 newList.put(key, value); 514 } 515 attributeList = newList; 516 } 517 for (NestedMapData child = firstChild; child != null; child = child.nextSibling) { 518 child.optimize(); 519 } 520 } 521 522 private void writeMultiline(Appendable out) throws IOException { 523 String marker = "EOM"; 524 while (getValue().contains(marker)) { 525 marker += System.nanoTime() % 10; 526 } 527 out.append(" << ").append(marker).append('\n').append(getValue()); 528 if (!getValue().endsWith("\n")) { 529 out.append('\n'); 530 } 531 out.append(marker).append('\n'); 532 } 533 534 private void indent(Appendable out, int indent) throws IOException { 535 for (int i = 0; i < indent; i++) { 536 out.append(" "); 537 } 538 } 539 540 private void writeNameAttrs(Appendable out) throws IOException { 541 // Output name 542 out.append(getName()); 543 if (attributeList != null && !attributeList.isEmpty()) { 544 // Output parameters. 545 out.append(" ["); 546 boolean first = true; 547 for (Map.Entry<String, String> attr : attributeList.entrySet()) { 548 if (first) { 549 first = false; 550 } else { 551 out.append(", "); 552 } 553 out.append(attr.getKey()); 554 if (attr.getValue().equals("1")) { 555 continue; 556 } 557 out.append(" = \""); 558 writeAttributeValue(out, attr.getValue()); 559 out.append('"'); 560 } 561 out.append(']'); 562 } 563 } 564 565 // Visible for testing 566 static void writeAttributeValue(Appendable out, String value) throws IOException { 567 for (int i = 0; i < value.length(); i++) { 568 char c = value.charAt(i); 569 switch (c) { 570 case '"': 571 out.append("\\\""); 572 break; 573 case '\n': 574 out.append("\\n"); 575 break; 576 case '\t': 577 out.append("\\t"); 578 break; 579 case '\\': 580 out.append("\\\\"); 581 break; 582 case '\r': 583 out.append("\\r"); 584 break; 585 default: 586 out.append(c); 587 } 588 } 589 } 590 591 /** 592 * A single instance of this is created per NestedMapData object. Its single method returns an 593 * iterator over the children of this node. 594 * <p> 595 * Note: This returns an iterator that starts with the first child at the time of iterator() being 596 * called, not when this Iterable object was handed to the code. This might result in slightly 597 * unexpected behavior if the children list is modified between when getChildren() is called and 598 * iterator is called on the returned object but this should not be an issue in practice as 599 * iterator is usually called immediately after getChildren(). 600 * 601 */ 602 private class IterableChildren implements Iterable<NestedMapData> { 603 public Iterator<NestedMapData> iterator() { 604 return new ChildrenIterator(followSymLinkToTheBitterEnd().firstChild); 605 } 606 } 607 608 /** 609 * Iterator implementation for children. We do not check for concurrent modification like in other 610 * Collection iterators. 611 */ 612 private static class ChildrenIterator implements Iterator<NestedMapData> { 613 NestedMapData current; 614 NestedMapData next; 615 616 ChildrenIterator(NestedMapData first) { 617 next = first; 618 current = null; 619 } 620 621 public boolean hasNext() { 622 return next != null; 623 } 624 625 public NestedMapData next() { 626 if (next == null) { 627 throw new NoSuchElementException(); 628 } 629 current = next; 630 next = next.nextSibling; 631 return current; 632 } 633 634 public void remove() { 635 if (current == null) { 636 throw new NoSuchElementException(); 637 } 638 current.severNode(); 639 current = null; 640 } 641 } 642 } 643