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