Home | History | Annotate | Download | only in data
      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