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