ForEachSqlNode.java

  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. import java.util.Map;
  18. import java.util.Optional;

  19. import org.apache.ibatis.parsing.GenericTokenParser;
  20. import org.apache.ibatis.session.Configuration;

  21. /**
  22.  * @author Clinton Begin
  23.  */
  24. public class ForEachSqlNode implements SqlNode {
  25.   public static final String ITEM_PREFIX = "__frch_";

  26.   private final ExpressionEvaluator evaluator = ExpressionEvaluator.INSTANCE;
  27.   private final String collectionExpression;
  28.   private final Boolean nullable;
  29.   private final SqlNode contents;
  30.   private final String open;
  31.   private final String close;
  32.   private final String separator;
  33.   private final String item;
  34.   private final String index;
  35.   private final Configuration configuration;

  36.   /**
  37.    * @deprecated Since 3.5.9, use the
  38.    *             {@link #ForEachSqlNode(Configuration, SqlNode, String, Boolean, String, String, String, String, String)}.
  39.    */
  40.   @Deprecated
  41.   public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index,
  42.       String item, String open, String close, String separator) {
  43.     this(configuration, contents, collectionExpression, null, index, item, open, close, separator);
  44.   }

  45.   /**
  46.    * @since 3.5.9
  47.    */
  48.   public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable,
  49.       String index, String item, String open, String close, String separator) {
  50.     this.collectionExpression = collectionExpression;
  51.     this.nullable = nullable;
  52.     this.contents = contents;
  53.     this.open = open;
  54.     this.close = close;
  55.     this.separator = separator;
  56.     this.index = index;
  57.     this.item = item;
  58.     this.configuration = configuration;
  59.   }

  60.   @Override
  61.   public boolean apply(DynamicContext context) {
  62.     Map<String, Object> bindings = context.getBindings();
  63.     final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
  64.         Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
  65.     if (iterable == null || !iterable.iterator().hasNext()) {
  66.       return true;
  67.     }
  68.     boolean first = true;
  69.     applyOpen(context);
  70.     int i = 0;
  71.     for (Object o : iterable) {
  72.       DynamicContext oldContext = context;
  73.       if (first || separator == null) {
  74.         context = new PrefixedContext(context, "");
  75.       } else {
  76.         context = new PrefixedContext(context, separator);
  77.       }
  78.       int uniqueNumber = context.getUniqueNumber();
  79.       // Issue #709
  80.       if (o instanceof Map.Entry) {
  81.         @SuppressWarnings("unchecked")
  82.         Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
  83.         applyIndex(context, mapEntry.getKey(), uniqueNumber);
  84.         applyItem(context, mapEntry.getValue(), uniqueNumber);
  85.       } else {
  86.         applyIndex(context, i, uniqueNumber);
  87.         applyItem(context, o, uniqueNumber);
  88.       }
  89.       contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
  90.       if (first) {
  91.         first = !((PrefixedContext) context).isPrefixApplied();
  92.       }
  93.       context = oldContext;
  94.       i++;
  95.     }
  96.     applyClose(context);
  97.     context.getBindings().remove(item);
  98.     context.getBindings().remove(index);
  99.     return true;
  100.   }

  101.   private void applyIndex(DynamicContext context, Object o, int i) {
  102.     if (index != null) {
  103.       context.bind(index, o);
  104.       context.bind(itemizeItem(index, i), o);
  105.     }
  106.   }

  107.   private void applyItem(DynamicContext context, Object o, int i) {
  108.     if (item != null) {
  109.       context.bind(item, o);
  110.       context.bind(itemizeItem(item, i), o);
  111.     }
  112.   }

  113.   private void applyOpen(DynamicContext context) {
  114.     if (open != null) {
  115.       context.appendSql(open);
  116.     }
  117.   }

  118.   private void applyClose(DynamicContext context) {
  119.     if (close != null) {
  120.       context.appendSql(close);
  121.     }
  122.   }

  123.   private static String itemizeItem(String item, int i) {
  124.     return ITEM_PREFIX + item + "_" + i;
  125.   }

  126.   private static class FilteredDynamicContext extends DynamicContext {
  127.     private final DynamicContext delegate;
  128.     private final int index;
  129.     private final String itemIndex;
  130.     private final String item;

  131.     public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item,
  132.         int i) {
  133.       super(configuration, null);
  134.       this.delegate = delegate;
  135.       this.index = i;
  136.       this.itemIndex = itemIndex;
  137.       this.item = item;
  138.     }

  139.     @Override
  140.     public Map<String, Object> getBindings() {
  141.       return delegate.getBindings();
  142.     }

  143.     @Override
  144.     public void bind(String name, Object value) {
  145.       delegate.bind(name, value);
  146.     }

  147.     @Override
  148.     public String getSql() {
  149.       return delegate.getSql();
  150.     }

  151.     @Override
  152.     public void appendSql(String sql) {
  153.       GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
  154.         String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
  155.         if (itemIndex != null && newContent.equals(content)) {
  156.           newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
  157.         }
  158.         return "#{" + newContent + "}";
  159.       });

  160.       delegate.appendSql(parser.parse(sql));
  161.     }

  162.     @Override
  163.     public int getUniqueNumber() {
  164.       return delegate.getUniqueNumber();
  165.     }

  166.   }

  167.   private class PrefixedContext extends DynamicContext {
  168.     private final DynamicContext delegate;
  169.     private final String prefix;
  170.     private boolean prefixApplied;

  171.     public PrefixedContext(DynamicContext delegate, String prefix) {
  172.       super(configuration, null);
  173.       this.delegate = delegate;
  174.       this.prefix = prefix;
  175.       this.prefixApplied = false;
  176.     }

  177.     public boolean isPrefixApplied() {
  178.       return prefixApplied;
  179.     }

  180.     @Override
  181.     public Map<String, Object> getBindings() {
  182.       return delegate.getBindings();
  183.     }

  184.     @Override
  185.     public void bind(String name, Object value) {
  186.       delegate.bind(name, value);
  187.     }

  188.     @Override
  189.     public void appendSql(String sql) {
  190.       if (!prefixApplied && sql != null && sql.trim().length() > 0) {
  191.         delegate.appendSql(prefix);
  192.         prefixApplied = true;
  193.       }
  194.       delegate.appendSql(sql);
  195.     }

  196.     @Override
  197.     public String getSql() {
  198.       return delegate.getSql();
  199.     }

  200.     @Override
  201.     public int getUniqueNumber() {
  202.       return delegate.getUniqueNumber();
  203.     }
  204.   }

  205. }