View Javadoc
1   /*
2    *    Copyright 2009-2025 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.apache.ibatis.scripting.xmltags;
17  
18  import java.util.Map;
19  import java.util.Optional;
20  
21  import org.apache.ibatis.parsing.GenericTokenParser;
22  import org.apache.ibatis.session.Configuration;
23  
24  /**
25   * @author Clinton Begin
26   */
27  public class ForEachSqlNode implements SqlNode {
28    public static final String ITEM_PREFIX = "__frch_";
29  
30    private final ExpressionEvaluator evaluator = ExpressionEvaluator.INSTANCE;
31    private final String collectionExpression;
32    private final Boolean nullable;
33    private final SqlNode contents;
34    private final String open;
35    private final String close;
36    private final String separator;
37    private final String item;
38    private final String index;
39    private final Configuration configuration;
40  
41    /**
42     * @deprecated Since 3.5.9, use the
43     *             {@link #ForEachSqlNode(Configuration, SqlNode, String, Boolean, String, String, String, String, String)}.
44     */
45    @Deprecated
46    public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index,
47        String item, String open, String close, String separator) {
48      this(configuration, contents, collectionExpression, null, index, item, open, close, separator);
49    }
50  
51    /**
52     * @since 3.5.9
53     */
54    public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable,
55        String index, String item, String open, String close, String separator) {
56      this.collectionExpression = collectionExpression;
57      this.nullable = nullable;
58      this.contents = contents;
59      this.open = open;
60      this.close = close;
61      this.separator = separator;
62      this.index = index;
63      this.item = item;
64      this.configuration = configuration;
65    }
66  
67    @Override
68    public boolean apply(DynamicContext context) {
69      Map<String, Object> bindings = context.getBindings();
70      final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
71          Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
72      if (iterable == null || !iterable.iterator().hasNext()) {
73        return true;
74      }
75      boolean first = true;
76      applyOpen(context);
77      int i = 0;
78      for (Object o : iterable) {
79        DynamicContext oldContext = context;
80        if (first || separator == null) {
81          context = new PrefixedContext(context, "");
82        } else {
83          context = new PrefixedContext(context, separator);
84        }
85        int uniqueNumber = context.getUniqueNumber();
86        // Issue #709
87        if (o instanceof Map.Entry) {
88          @SuppressWarnings("unchecked")
89          Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
90          applyIndex(context, mapEntry.getKey(), uniqueNumber);
91          applyItem(context, mapEntry.getValue(), uniqueNumber);
92        } else {
93          applyIndex(context, i, uniqueNumber);
94          applyItem(context, o, uniqueNumber);
95        }
96        contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
97        if (first) {
98          first = !((PrefixedContext) context).isPrefixApplied();
99        }
100       context = oldContext;
101       i++;
102     }
103     applyClose(context);
104     context.getBindings().remove(item);
105     context.getBindings().remove(index);
106     return true;
107   }
108 
109   private void applyIndex(DynamicContext context, Object o, int i) {
110     if (index != null) {
111       context.bind(index, o);
112       context.bind(itemizeItem(index, i), o);
113     }
114   }
115 
116   private void applyItem(DynamicContext context, Object o, int i) {
117     if (item != null) {
118       context.bind(item, o);
119       context.bind(itemizeItem(item, i), o);
120     }
121   }
122 
123   private void applyOpen(DynamicContext context) {
124     if (open != null) {
125       context.appendSql(open);
126     }
127   }
128 
129   private void applyClose(DynamicContext context) {
130     if (close != null) {
131       context.appendSql(close);
132     }
133   }
134 
135   private static String itemizeItem(String item, int i) {
136     return ITEM_PREFIX + item + "_" + i;
137   }
138 
139   private static class FilteredDynamicContext extends DynamicContext {
140     private final DynamicContext delegate;
141     private final int index;
142     private final String itemIndex;
143     private final String item;
144 
145     public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item,
146         int i) {
147       super(configuration, null);
148       this.delegate = delegate;
149       this.index = i;
150       this.itemIndex = itemIndex;
151       this.item = item;
152     }
153 
154     @Override
155     public Map<String, Object> getBindings() {
156       return delegate.getBindings();
157     }
158 
159     @Override
160     public void bind(String name, Object value) {
161       delegate.bind(name, value);
162     }
163 
164     @Override
165     public String getSql() {
166       return delegate.getSql();
167     }
168 
169     @Override
170     public void appendSql(String sql) {
171       GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
172         String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
173         if (itemIndex != null && newContent.equals(content)) {
174           newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
175         }
176         return "#{" + newContent + "}";
177       });
178 
179       delegate.appendSql(parser.parse(sql));
180     }
181 
182     @Override
183     public int getUniqueNumber() {
184       return delegate.getUniqueNumber();
185     }
186 
187   }
188 
189   private class PrefixedContext extends DynamicContext {
190     private final DynamicContext delegate;
191     private final String prefix;
192     private boolean prefixApplied;
193 
194     public PrefixedContext(DynamicContext delegate, String prefix) {
195       super(configuration, null);
196       this.delegate = delegate;
197       this.prefix = prefix;
198       this.prefixApplied = false;
199     }
200 
201     public boolean isPrefixApplied() {
202       return prefixApplied;
203     }
204 
205     @Override
206     public Map<String, Object> getBindings() {
207       return delegate.getBindings();
208     }
209 
210     @Override
211     public void bind(String name, Object value) {
212       delegate.bind(name, value);
213     }
214 
215     @Override
216     public void appendSql(String sql) {
217       if (!prefixApplied && sql != null && sql.trim().length() > 0) {
218         delegate.appendSql(prefix);
219         prefixApplied = true;
220       }
221       delegate.appendSql(sql);
222     }
223 
224     @Override
225     public String getSql() {
226       return delegate.getSql();
227     }
228 
229     @Override
230     public int getUniqueNumber() {
231       return delegate.getUniqueNumber();
232     }
233   }
234 
235 }