View Javadoc
1   /*
2    *    Copyright 2012-2024 the original author or authors.
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    *       https://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  package org.mybatis.scripting.velocity;
17  
18  import java.io.IOException;
19  import java.io.StringWriter;
20  import java.io.Writer;
21  import java.util.Iterator;
22  
23  import org.apache.velocity.context.ChainedInternalContextAdapter;
24  import org.apache.velocity.context.InternalContextAdapter;
25  import org.apache.velocity.exception.TemplateInitException;
26  import org.apache.velocity.exception.VelocityException;
27  import org.apache.velocity.runtime.RuntimeServices;
28  import org.apache.velocity.runtime.directive.Directive;
29  import org.apache.velocity.runtime.directive.Scope;
30  import org.apache.velocity.runtime.directive.StopCommand;
31  import org.apache.velocity.runtime.parser.node.ASTReference;
32  import org.apache.velocity.runtime.parser.node.ASTStringLiteral;
33  import org.apache.velocity.runtime.parser.node.Node;
34  import org.apache.velocity.runtime.parser.node.StandardParserTreeConstants;
35  import org.apache.velocity.util.introspection.Info;
36  
37  /**
38   * #repeat($collection $item SEP OPEN CLOSE).
39   */
40  public class RepeatDirective extends Directive {
41  
42    protected static final int MAX_IN_CLAUSE_SIZE = 1000;
43  
44    /** Immutable fields */
45    private String var;
46    private String open = "";
47    private String close = "";
48    private String separator = "";
49    protected Info uberInfo;
50  
51    @Override
52    public String getName() {
53      return "repeat";
54    }
55  
56    @Override
57    public void init(RuntimeServices rs, InternalContextAdapter context, Node node) {
58      super.init(rs, context, node);
59      final int n = node.jjtGetNumChildren() - 1;
60      for (int i = 1; i < n; i++) {
61        Node child = node.jjtGetChild(i);
62        if (i == 1) {
63          if (child.getType() == StandardParserTreeConstants.JJTREFERENCE) {
64            this.var = ((ASTReference) child).getRootString();
65          } else {
66            throw new TemplateInitException("Syntax error", getTemplateName(), getLine(), getColumn());
67          }
68        } else if (child.getType() == StandardParserTreeConstants.JJTSTRINGLITERAL) {
69          String value = (String) ((ASTStringLiteral) child).value(context);
70          switch (i) {
71            case 2:
72              this.separator = value;
73              break;
74            case 3:
75              this.open = value;
76              break;
77            case 4:
78              this.close = value;
79              break;
80            default:
81              break;
82          }
83        } else {
84          throw new TemplateInitException("Syntax error", getTemplateName(), getLine(), getColumn());
85        }
86      }
87      this.uberInfo = new Info(this.getTemplateName(), getLine(), getColumn());
88    }
89  
90    @Override
91    public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException {
92  
93      Object listObject = node.jjtGetChild(0).value(context);
94  
95      if (listObject == null) {
96        return false;
97      }
98  
99      Iterator<?> i = null;
100 
101     try {
102       i = this.rsvc.getUberspect().getIterator(listObject, this.uberInfo);
103     } catch (RuntimeException e) {
104       throw e;
105     } catch (Exception ee) {
106       String msg = "Error getting iterator for #repeat at " + this.uberInfo;
107       this.rsvc.getLog().error(msg, ee);
108       throw new VelocityException(msg, ee);
109     }
110 
111     if (i == null) {
112       throw new VelocityException("Invalid collection");
113     }
114 
115     int counter = 0;
116     boolean maxNbrLoopsExceeded = false;
117     Object o = context.get(this.var);
118 
119     ParameterMappingCollector collector = (ParameterMappingCollector) context
120         .get(SQLScriptSource.MAPPING_COLLECTOR_KEY);
121     String savedItemKey = collector.getItemKey();
122     collector.setItemKey(this.var);
123     RepeatScope foreach = new RepeatScope(this, context.get(getName()), this.var);
124     context.put(getName(), foreach);
125 
126     NullHolderContext nullHolderContext = null;
127     StringWriter buffer = new StringWriter();
128     while (!maxNbrLoopsExceeded && i.hasNext()) {
129       Object value = i.next();
130       put(context, this.var, value);
131       foreach.index++;
132       foreach.hasNext = i.hasNext();
133 
134       try {
135         if (value == null) {
136           if (nullHolderContext == null) {
137             nullHolderContext = new NullHolderContext(this.var, context);
138           }
139           node.jjtGetChild(node.jjtGetNumChildren() - 1).render(nullHolderContext, buffer);
140         } else {
141           node.jjtGetChild(node.jjtGetNumChildren() - 1).render(context, buffer);
142         }
143       } catch (StopCommand stop) {
144         if (stop.isFor(this)) {
145           break;
146         }
147         clean(context, o, collector, savedItemKey);
148         // close does not perform any action and this is here
149         // to avoid eclipse reporting possible leak.
150         buffer.close();
151         throw stop;
152       }
153 
154       counter++;
155       maxNbrLoopsExceeded = counter >= MAX_IN_CLAUSE_SIZE;
156 
157       if (i.hasNext() && !maxNbrLoopsExceeded) {
158         buffer.append(this.separator);
159       }
160 
161     }
162     String content = buffer.toString().trim();
163     if (!"".equals(content)) {
164       writer.append(this.open);
165       writer.append(content);
166       writer.append(this.close);
167     }
168     clean(context, o, collector, savedItemKey);
169     return true;
170 
171   }
172 
173   protected void clean(InternalContextAdapter context, Object o, ParameterMappingCollector collector,
174       String savedItemKey) {
175     if (o != null) {
176       context.put(this.var, o);
177     } else {
178       context.remove(this.var);
179     }
180     collector.setItemKey(savedItemKey);
181     postRender(context);
182   }
183 
184   protected void put(InternalContextAdapter context, String key, Object value) {
185     context.put(key, value);
186   }
187 
188   @Override
189   public int getType() {
190     return BLOCK;
191   }
192 
193   public static class RepeatScope extends Scope {
194 
195     protected int index = -1;
196     protected boolean hasNext = false;
197     protected final String var;
198 
199     public RepeatScope(Object newOwner, Object replaces, String newVar) {
200       super(newOwner, replaces);
201       this.var = newVar;
202     }
203 
204     public int getIndex() {
205       return this.index;
206     }
207 
208     public int getCount() {
209       return this.index + 1;
210     }
211 
212     public boolean hasNext() {
213       return getHasNext();
214     }
215 
216     public boolean getHasNext() {
217       return this.hasNext;
218     }
219 
220     public boolean isFirst() {
221       return this.index < 1;
222     }
223 
224     public boolean getFirst() {
225       return isFirst();
226     }
227 
228     public boolean isLast() {
229       return !this.hasNext;
230     }
231 
232     public boolean getLast() {
233       return isLast();
234     }
235 
236     public String getVar() {
237       return this.var;
238     }
239 
240   }
241 
242   protected static class NullHolderContext extends ChainedInternalContextAdapter {
243 
244     private String loopVariableKey = "";
245     private boolean active = true;
246 
247     protected NullHolderContext(String key, InternalContextAdapter context) {
248       super(context);
249       if (key != null) {
250         this.loopVariableKey = key;
251       }
252     }
253 
254     @Override
255     public Object get(String key) {
256       return (this.active && this.loopVariableKey.equals(key)) ? null : super.get(key);
257     }
258 
259     @Override
260     public Object put(String key, Object value) {
261       if (this.loopVariableKey.equals(key) && (value == null)) {
262         this.active = true;
263       }
264 
265       return super.put(key, value);
266     }
267 
268     @Override
269     public Object remove(String key) {
270       if (this.loopVariableKey.equals(key)) {
271         this.active = false;
272       }
273       return super.remove(key);
274     }
275   }
276 
277   @Override
278   public boolean isScopeProvided() {
279     return true;
280   }
281 
282 }