View Javadoc
1   /*
2    *    Copyright 2016-2026 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.dynamic.sql.where.render;
17  
18  import java.util.List;
19  import java.util.Objects;
20  import java.util.Optional;
21  import java.util.function.Function;
22  import java.util.stream.Collectors;
23  
24  import org.mybatis.dynamic.sql.AndOrCriteriaGroup;
25  import org.mybatis.dynamic.sql.ColumnAndConditionCriterion;
26  import org.mybatis.dynamic.sql.CriteriaGroup;
27  import org.mybatis.dynamic.sql.ExistsCriterion;
28  import org.mybatis.dynamic.sql.ExistsPredicate;
29  import org.mybatis.dynamic.sql.NotCriterion;
30  import org.mybatis.dynamic.sql.NullCriterion;
31  import org.mybatis.dynamic.sql.SqlCriterion;
32  import org.mybatis.dynamic.sql.SqlCriterionVisitor;
33  import org.mybatis.dynamic.sql.render.RenderingContext;
34  import org.mybatis.dynamic.sql.select.render.SubQueryRenderer;
35  import org.mybatis.dynamic.sql.util.FragmentAndParameters;
36  import org.mybatis.dynamic.sql.util.FragmentCollector;
37  
38  /**
39   * Renders a {@link SqlCriterion} to a {@link RenderedCriterion}. The process is complex because all conditions
40   * may or may not be a candidate for rendering. For example, "isEqualWhenPresent" will not render when the value
41   * is null. It is also complex because SqlCriterion may or may not include sub-criteria.
42   *
43   * <p>Rendering is a recursive process. The renderer will recurse into each sub-criteria - which may also
44   * contain further sub-criteria - until all possible sub-criteria are rendered into a single fragment. So, for example,
45   * the fragment may end up looking like:
46   *
47   * <pre>
48   *     col1 = ? and (col2 = ? or (col3 = ? and col4 = ?))
49   * </pre>
50   *
51   * <p>It is also possible that the end result will be empty if all criteria and sub-criteria are not valid for
52   * rendering.
53   *
54   * @author Jeff Butler
55   */
56  public class CriterionRenderer implements SqlCriterionVisitor<Optional<RenderedCriterion>> {
57      private final RenderingContext renderingContext;
58  
59      public CriterionRenderer(RenderingContext renderingContext) {
60          this.renderingContext = Objects.requireNonNull(renderingContext);
61      }
62  
63      @Override
64      public <T> Optional<RenderedCriterion> visit(ColumnAndConditionCriterion<T> criterion) {
65          Optional<FragmentAndParameters> initialCriterion = renderColumnAndCondition(criterion);
66          List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(criterion.subCriteria());
67  
68          return initialCriterion.map(fp -> calculateRenderedCriterion(fp, renderedSubCriteria, this::calculateFragment))
69                  .orElseGet(() -> calculateRenderedCriterion(renderedSubCriteria, this::calculateFragment));
70      }
71  
72      @Override
73      public Optional<RenderedCriterion> visit(ExistsCriterion criterion) {
74          FragmentAndParameters initialCriterion = renderExists(criterion);
75          List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(criterion.subCriteria());
76  
77          return calculateRenderedCriterion(initialCriterion, renderedSubCriteria, this::calculateFragment);
78      }
79  
80      @Override
81      public Optional<RenderedCriterion> visit(CriteriaGroup criterion) {
82          return renderCriteriaGroup(criterion, this::calculateFragment);
83      }
84  
85      @Override
86      public Optional<RenderedCriterion> visit(NotCriterion criterion) {
87          return renderCriteriaGroup(criterion, this::calculateNotFragment);
88      }
89  
90      @Override
91      public Optional<RenderedCriterion> visit(NullCriterion criterion) {
92          return Optional.empty();
93      }
94  
95      private Optional<RenderedCriterion> renderCriteriaGroup(CriteriaGroup criterion,
96                                                              Function<FragmentCollector, String> fragmentCalculator) {
97          return render(criterion.initialCriterion(), criterion.subCriteria(), fragmentCalculator);
98      }
99  
100     public Optional<RenderedCriterion> render(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria,
101                                               Function<FragmentCollector, String> fragmentCalculator) {
102         Optional<FragmentAndParameters> fragmentAndParameters = initialCriterion.accept(this)
103                 .map(RenderedCriterion::fragmentAndParameters);
104         List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(subCriteria);
105 
106         return fragmentAndParameters.map(fp -> calculateRenderedCriterion(fp, renderedSubCriteria, fragmentCalculator))
107                 .orElseGet(() -> calculateRenderedCriterion(renderedSubCriteria, fragmentCalculator));
108     }
109 
110     private <T> Optional<FragmentAndParameters> renderColumnAndCondition(ColumnAndConditionCriterion<T> criterion) {
111         if (criterion.condition().shouldRender(renderingContext)) {
112             return Optional.of(renderCondition(criterion));
113         } else {
114             criterion.condition().renderingSkipped();
115             return Optional.empty();
116         }
117     }
118 
119     private FragmentAndParameters renderExists(ExistsCriterion criterion) {
120         ExistsPredicate existsPredicate = criterion.existsPredicate();
121         return SubQueryRenderer.withSelectModel(existsPredicate.selectModelBuilder().build())
122                 .withRenderingContext(renderingContext)
123                 .withPrefix(existsPredicate.operator() + " (") //$NON-NLS-1$
124                 .withSuffix(")") //$NON-NLS-1$
125                 .build()
126                 .render();
127     }
128 
129     private List<RenderedCriterion> renderSubCriteria(List<AndOrCriteriaGroup> subCriteria) {
130         return subCriteria.stream().map(this::renderAndOrCriteriaGroup)
131                 .flatMap(Optional::stream)
132                 .toList();
133     }
134 
135     private Optional<RenderedCriterion> renderAndOrCriteriaGroup(AndOrCriteriaGroup criterion) {
136         return render(criterion.initialCriterion(), criterion.subCriteria(), this::calculateFragment)
137                 .map(rc -> rc.withConnector(criterion.connector()));
138     }
139 
140     private Optional<RenderedCriterion> calculateRenderedCriterion(FragmentAndParameters initialCriterion,
141             List<RenderedCriterion> renderedSubCriteria, Function<FragmentCollector, String> fragmentCalculator) {
142         return Optional.of(calculateRenderedCriterion(
143                 collectSqlFragments(initialCriterion, renderedSubCriteria), fragmentCalculator));
144     }
145 
146     private RenderedCriterion calculateRenderedCriterion(FragmentCollector fragmentCollector,
147                                                          Function<FragmentCollector, String> fragmentCalculator) {
148         FragmentAndParameters fragmentAndParameters = FragmentAndParameters
149                 .withFragment(fragmentCalculator.apply(fragmentCollector))
150                 .withParameters(fragmentCollector.parameters())
151                 .build();
152 
153         return new RenderedCriterion.Builder()
154                 .withFragmentAndParameters(fragmentAndParameters)
155                 .build();
156     }
157 
158     private Optional<RenderedCriterion> calculateRenderedCriterion(List<RenderedCriterion> renderedSubCriteria,
159             Function<FragmentCollector, String> fragmentCalculator) {
160         return collectSqlFragments(renderedSubCriteria).map(fc -> calculateRenderedCriterion(fc, fragmentCalculator));
161     }
162 
163     private <T> FragmentAndParameters renderCondition(ColumnAndConditionCriterion<T> criterion) {
164         return new ColumnAndConditionRenderer.Builder<T>()
165                 .withColumn(criterion.column())
166                 .withCondition(criterion.condition())
167                 .withRenderingContext(renderingContext)
168                 .build()
169                 .render();
170     }
171 
172     /**
173      * This method encapsulates the logic of building a collection of fragments from an initial condition
174      * and a list of rendered sub criteria. In this overload we know there is an initial condition
175      * and there may be subcriteria. The collector will contain the initial condition and any rendered subcriteria
176      * in order.
177      *
178      * @param initialCondition - may not be null. If there is no initial condition, then use the other overload
179      * @param renderedSubCriteria - a list of previously rendered sub criteria. The sub criteria will all
180      *                            have connectors (either an AND or an OR)
181      * @return a fragment collector whose fragments represent the final calculated list of fragments and parameters.
182      *     The fragment collector can be used to calculate the single composed fragment - either as a where clause, or
183      *     a valid rendered sub criteria in the case of a recursive call.
184      */
185     private FragmentCollector collectSqlFragments(FragmentAndParameters initialCondition,
186                                                   List<RenderedCriterion> renderedSubCriteria) {
187         return renderedSubCriteria.stream()
188                 .map(RenderedCriterion::fragmentAndParametersWithConnector)
189                 .collect(FragmentCollector.collect(initialCondition));
190     }
191 
192     /**
193      * This method encapsulates the logic of building a collection of fragments from a list of rendered sub criteria.
194      * In this overload we take the initial condition to be the first element in the subcriteria list.
195      * The collector will contain the rendered subcriteria in order. However, the connector from the first rendered
196      * sub criterion will be removed. This to avoid generating an invalid where clause like "where and a < 3"
197      *
198      * @param renderedSubCriteria - a list of previously rendered sub criteria. The sub criteria will all
199      *                            have connectors (either an AND or an OR)
200      * @return a fragment collector whose fragments represent the final calculated list of fragments and parameters.
201      *     The fragment collector can be used to calculate the single composed fragment - either as a where clause, or
202      *     a valid rendered sub criteria in the case of a recursive call.
203      */
204     private Optional<FragmentCollector> collectSqlFragments(List<RenderedCriterion> renderedSubCriteria) {
205         if (renderedSubCriteria.isEmpty()) {
206             return Optional.empty();
207         }
208 
209         FragmentAndParameters firstCondition = renderedSubCriteria.get(0).fragmentAndParameters();
210 
211         FragmentCollector fc = renderedSubCriteria.stream()
212                 .skip(1)
213                 .map(RenderedCriterion::fragmentAndParametersWithConnector)
214                 .collect(FragmentCollector.collect(firstCondition));
215 
216         return Optional.of(fc);
217     }
218 
219     private String calculateFragment(FragmentCollector collector) {
220         if (collector.hasMultipleFragments()) {
221             return collector.collectFragments(
222                     Collectors.joining(" ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
223         } else {
224             return collector.firstFragment().orElse(""); //$NON-NLS-1$
225         }
226     }
227 
228     private String calculateNotFragment(FragmentCollector collector) {
229         if (collector.hasMultipleFragments()) {
230             return collector.collectFragments(
231                     Collectors.joining(" ", "not (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
232         } else {
233             return collector.firstFragment().map(s -> "not " + s).orElse(""); //$NON-NLS-1$ //$NON-NLS-2$
234         }
235     }
236 }