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
17package com.google.clearsilver.jsilver.data;
18
19
20import java.io.IOException;
21import java.util.Collections;
22import java.util.HashMap;
23import java.util.Iterator;
24import java.util.Map;
25import 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 */
36public 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