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
19import com.google.clearsilver.jsilver.autoescape.EscapeMode;
20
21import java.io.IOException;
22import java.util.Map;
23
24/**
25 * This is the basic implementation of the DataContext. It stores the root Data node and a stack of
26 * Data objects that hold local variables. By definition, local variables are limited to single HDF
27 * names, with no '.' allowed. We use this to limit our search in the stack for the first occurence
28 * of the first chunk in the variable name.
29 */
30public class DefaultDataContext implements DataContext {
31
32  /**
33   * Root node of the Data structure used by the current context.
34   */
35  private final Data rootData;
36
37  /**
38   * Head of the linked list of local variables, starting with the newest variable created.
39   */
40  private LocalVariable head = null;
41
42  /**
43   * Indicates whether the renderer has pushed a new variable scope but no variable has been created
44   * yet.
45   */
46  private boolean newScope = false;
47
48  public DefaultDataContext(Data data) {
49    if (data == null) {
50      throw new IllegalArgumentException("rootData is null");
51    }
52    this.rootData = data;
53  }
54
55  @Override
56  public Data getRootData() {
57    return rootData;
58  }
59
60  /**
61   * Starts a new variable scope. It is illegal to call this twice in a row without declaring a
62   * local variable.
63   */
64  @Override
65  public void pushVariableScope() {
66    if (newScope) {
67      throw new IllegalStateException("PushVariableScope called twice with no "
68          + "variables declared in between.");
69    }
70    newScope = true;
71  }
72
73  /**
74   * Removes the current variable scope and references to the variables in it. It is illegal to call
75   * this more times than {@link #pushVariableScope} has been called.
76   */
77  @Override
78  public void popVariableScope() {
79    if (newScope) {
80      // We pushed but created no local variables.
81      newScope = false;
82    } else {
83      // Note, this will throw a NullPointerException if there is no scope to
84      // pop.
85      head = head.nextScope;
86    }
87  }
88
89  @Override
90  public void createLocalVariableByValue(String name, String value) {
91    createLocalVariableByValue(name, value, EscapeMode.ESCAPE_NONE);
92  }
93
94  @Override
95  public void createLocalVariableByValue(String name, String value, EscapeMode mode) {
96    LocalVariable local = createLocalVariable(name);
97    local.value = value;
98    local.isPath = false;
99    local.setEscapeMode(mode);
100  }
101
102  @Override
103  public void createLocalVariableByValue(String name, String value, boolean isFirst, boolean isLast) {
104    LocalVariable local = createLocalVariable(name);
105    local.value = value;
106    local.isPath = false;
107    local.isFirst = isFirst;
108    local.isLast = isLast;
109  }
110
111  @Override
112  public void createLocalVariableByPath(String name, String path) {
113    LocalVariable local = createLocalVariable(name);
114    local.value = path;
115    local.isPath = true;
116  }
117
118  private LocalVariable createLocalVariable(String name) {
119    if (head == null && !newScope) {
120      throw new IllegalStateException("Must call pushVariableScope before "
121          + "creating local variable.");
122    }
123    // First look for a local variable with the same name in the current scope
124    // and return that if it exists. If we don't do this then loops and each
125    // can cause the list of local variables to explode.
126    //
127    // We only look at the first local variable (head) if it is part of the
128    // current scope (we're not defining a new scope). Since each and loop
129    // variables are always in their own scope, there would only be one variable
130    // in the current scope if it was a reuse case. For macro calls (which are
131    // the only other way createLocalVariable is called multiple times in a
132    // single scope and thus head may not be the only local variable in the
133    // current scope) it is illegal to use the same argument name more than
134    // once. Therefore we don't need to worry about checking to see if the new
135    // local variable name matches beyond the first local variable in the
136    // current scope.
137
138    if (!newScope && head != null && name.equals(head.name)) {
139      // Clear out the fields that aren't set by the callers.
140      head.isFirst = true;
141      head.isLast = true;
142      head.node = null;
143      return head;
144    }
145
146    LocalVariable local = new LocalVariable();
147    local.name = name;
148    local.next = head;
149    if (newScope) {
150      local.nextScope = head;
151      newScope = false;
152    } else if (head != null) {
153      local.nextScope = head.nextScope;
154    } else {
155      local.nextScope = null;
156    }
157    head = local;
158    return local;
159  }
160
161  @Override
162  public Data findVariable(String name, boolean create) {
163    return findVariable(name, create, head);
164  }
165
166  @Override
167  public EscapeMode findVariableEscapeMode(String name) {
168    Data var = findVariable(name, false);
169    if (var == null) {
170      return EscapeMode.ESCAPE_NONE;
171    } else {
172      return var.getEscapeMode();
173    }
174  }
175
176  private Data findVariable(String name, boolean create, LocalVariable start) {
177    // When traversing the stack, we first look for the first chunk of the
178    // name. This is so we respect scope. If we are searching for 'foo.bar'
179    // and 'foo' is defined in many scopes, we should stop searching
180    // after the first time we find 'foo', even if that local variable does
181    // not have a child 'bar'.
182    String firstChunk = name;
183    int dot = name.indexOf('.');
184    if (dot != -1) {
185      firstChunk = name.substring(0, dot);
186    }
187
188    LocalVariable curr = start;
189
190    while (curr != null) {
191      if (curr.name.equals(firstChunk)) {
192        if (curr.isPath) {
193          // The local variable references another Data node, dereference it.
194          if (curr.node == null) {
195            // We haven't resolved the path yet. Do it now and cache it if
196            // not null. Note we begin looking for the dereferenced in the next
197            // scope.
198            curr.node = findVariable(curr.value, create, curr.nextScope);
199            if (curr.node == null) {
200              // Node does not exist. Any children won't either.
201              return null;
202            }
203          }
204          // We have a reference to the Data node directly. Use it.
205          if (dot == -1) {
206            // This is the node we're interested in.
207            return curr.node;
208          } else {
209            if (create) {
210              return curr.node.createChild(name.substring(dot + 1));
211            } else {
212              return curr.node.getChild(name.substring(dot + 1));
213            }
214          }
215        } else {
216          // This is a literal value local variable. It has no children, nor
217          // can it. We want to throw an error on creation of children.
218          if (dot == -1) {
219            return curr;
220          }
221          if (create) {
222            throw new IllegalStateException("Cannot create children of a "
223                + "local literal variable");
224          } else {
225            // No children.
226            return null;
227          }
228        }
229      }
230      curr = curr.next;
231    }
232    if (create) {
233      return rootData.createChild(name);
234    } else {
235      return rootData.getChild(name);
236    }
237  }
238
239  /**
240   * This class holds the name and value/path of a local variable. Objects of this type should only
241   * be exposed outside of this class for value-based local variables.
242   * <p>
243   * Fields are not private to avoid the performance overhead of hidden access methods used for
244   * outer classes to access private fields of inner classes.
245   */
246  private static class LocalVariable extends AbstractData {
247    // Pointer to next LocalVariable in the list.
248    LocalVariable next;
249    // Pointer to the first LocalVariable in the next scope down.
250    LocalVariable nextScope;
251
252    String name;
253    String value;
254    // True if value represents the path to another Data variable.
255    boolean isPath;
256    // Once the path resolves to a valid Data node, store it here to avoid
257    // refetching.
258    Data node = null;
259
260    // Used only for loop local variables
261    boolean isFirst = true;
262    boolean isLast = true;
263
264    public String getName() {
265      return name;
266    }
267
268    public String getValue() {
269      return value;
270    }
271
272    public void setValue(String value) {
273      this.value = value;
274    }
275
276    public String getFullPath() {
277      return name;
278    }
279
280    public void setAttribute(String key, String value) {
281      throw new UnsupportedOperationException("Not allowed on local variables.");
282    }
283
284    public String getAttribute(String key) {
285      return null;
286    }
287
288    public boolean hasAttribute(String key) {
289      return false;
290    }
291
292    public int getAttributeCount() {
293      return 0;
294    }
295
296    public Iterable<Map.Entry<String, String>> getAttributes() {
297      return null;
298    }
299
300    public Data getRoot() {
301      return null;
302    }
303
304    public Data getParent() {
305      return null;
306    }
307
308    public boolean isFirstSibling() {
309      return isFirst;
310    }
311
312    public boolean isLastSibling() {
313      return isLast;
314    }
315
316    public Data getNextSibling() {
317      throw new UnsupportedOperationException("Not allowed on local variables.");
318    }
319
320    public int getChildCount() {
321      return 0;
322    }
323
324    public Iterable<? extends Data> getChildren() {
325      return null;
326    }
327
328    public Data getChild(String path) {
329      return null;
330    }
331
332    public Data createChild(String path) {
333      throw new UnsupportedOperationException("Not allowed on local variables.");
334    }
335
336    public void removeTree(String path) {
337      throw new UnsupportedOperationException("Not allowed on local variables.");
338    }
339
340    public void setSymlink(String sourcePath, String destinationPath) {
341      throw new UnsupportedOperationException("Not allowed on local variables.");
342    }
343
344    public void setSymlink(String sourcePath, Data destination) {
345      throw new UnsupportedOperationException("Not allowed on local variables.");
346    }
347
348    public void setSymlink(Data symLink) {
349      throw new UnsupportedOperationException("Not allowed on local variables.");
350    }
351
352    public Data getSymlink() {
353      return this;
354    }
355
356    public void copy(String toPath, Data from) {
357      throw new UnsupportedOperationException("Not allowed on local variables.");
358    }
359
360    public void copy(Data from) {
361      throw new UnsupportedOperationException("Not allowed on local variables.");
362    }
363
364    public String getValue(String path, String defaultValue) {
365      throw new UnsupportedOperationException("Not allowed on local variables.");
366    }
367
368    public void write(Appendable out, int indent) throws IOException {
369      for (int i = 0; i < indent; i++) {
370        out.append("  ");
371      }
372      out.append(getName()).append(" = ").append(getValue());
373    }
374  }
375}
376