• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
NestedMapData()72   public NestedMapData() {
73     name = null;
74     parent = null;
75     root = this;
76   }
77 
NestedMapData(String name, NestedMapData parent, NestedMapData root)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    */
createChildNode(String chunk)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 
severNode()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    */
getName()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    */
getPathName(StringBuilder sb)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    */
getFullPath()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    */
getValue()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    */
setValue(String value)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 
setAttribute(String key, String value)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 
getAttribute(String key)223   public String getAttribute(String key) {
224     return attributeList == null ? null : attributeList.get(key);
225   }
226 
hasAttribute(String key)227   public boolean hasAttribute(String key) {
228     return attributeList != null && attributeList.containsKey(key);
229   }
230 
getAttributeCount()231   public int getAttributeCount() {
232     return attributeList == null ? 0 : attributeList.size();
233   }
234 
getAttributes()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    */
getRoot()248   public Data getRoot() {
249     return root;
250   }
251 
252   /**
253    * Get the parent node.
254    */
getParent()255   public Data getParent() {
256     return parent;
257   }
258 
259   /**
260    * Is this the first of its siblings?
261    */
262   @Override
isFirstSibling()263   public boolean isFirstSibling() {
264     return prevSibling == null;
265   }
266 
267   /**
268    * Is this the last of its siblings?
269    */
270   @Override
isLastSibling()271   public boolean isLastSibling() {
272     return nextSibling == null;
273   }
274 
getNextSibling()275   public Data getNextSibling() {
276     return nextSibling;
277   }
278 
279   /**
280    * Returns number of child nodes.
281    */
282   @Override
getChildCount()283   public int getChildCount() {
284     return followSymLinkToTheBitterEnd().childCount;
285   }
286 
287   /**
288    * Returns children of this node.
289    */
290   @Override
getChildren()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    */
getChild(String path)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    */
createChild(String path)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    */
getChildNode(String name)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    */
removeTree(String path)361   public void removeTree(String path) {
362     NestedMapData removed = getChild(path);
363     if (removed != null) {
364       removed.severNode();
365     }
366   }
367 
followSymLinkToTheBitterEnd()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    */
setSymlink(String sourcePath, String destinationPath)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    */
setSymlink(String sourcePath, Data destination)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    */
setSymlink(Data symLink)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    */
getSymlink()411   public Data getSymlink() {
412     return symLink;
413   }
414 
415   // ************************ Copy *************************
416 
copy(String toPath, Data from)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 
copy(Data from)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    */
write(Appendable out, int indent)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
optimize()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 
writeMultiline(Appendable out)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 
indent(Appendable out, int indent)534   private void indent(Appendable out, int indent) throws IOException {
535     for (int i = 0; i < indent; i++) {
536       out.append("  ");
537     }
538   }
539 
writeNameAttrs(Appendable out)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
writeAttributeValue(Appendable out, String value)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> {
iterator()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 
ChildrenIterator(NestedMapData first)616     ChildrenIterator(NestedMapData first) {
617       next = first;
618       current = null;
619     }
620 
hasNext()621     public boolean hasNext() {
622       return next != null;
623     }
624 
next()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 
remove()634     public void remove() {
635       if (current == null) {
636         throw new NoSuchElementException();
637       }
638       current.severNode();
639       current = null;
640     }
641   }
642 }
643