CriterionRenderer.java
/*
* Copyright 2016-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.dynamic.sql.where.render;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.mybatis.dynamic.sql.AndOrCriteriaGroup;
import org.mybatis.dynamic.sql.ColumnAndConditionCriterion;
import org.mybatis.dynamic.sql.CriteriaGroup;
import org.mybatis.dynamic.sql.ExistsCriterion;
import org.mybatis.dynamic.sql.ExistsPredicate;
import org.mybatis.dynamic.sql.NotCriterion;
import org.mybatis.dynamic.sql.SqlCriterion;
import org.mybatis.dynamic.sql.SqlCriterionVisitor;
import org.mybatis.dynamic.sql.render.RenderingContext;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
import org.mybatis.dynamic.sql.util.FragmentCollector;
/**
* Renders a {@link SqlCriterion} to a {@link RenderedCriterion}. The process is complex because all conditions
* may or may not be a candidate for rendering. For example, "isEqualWhenPresent" will not render when the value
* is null. It is also complex because SqlCriterion may or may not include sub-criteria.
*
* <p>Rendering is a recursive process. The renderer will recurse into each sub-criteria - which may also
* contain further sub-criteria - until all possible sub-criteria are rendered into a single fragment. So, for example,
* the fragment may end up looking like:
*
* <pre>
* col1 = ? and (col2 = ? or (col3 = ? and col4 = ?))
* </pre>
*
* <p>It is also possible that the end result will be empty if all criteria and sub-criteria are not valid for
* rendering.
*
* @author Jeff Butler
*/
public class CriterionRenderer implements SqlCriterionVisitor<Optional<RenderedCriterion>> {
private final RenderingContext renderingContext;
public CriterionRenderer(RenderingContext renderingContext) {
this.renderingContext = Objects.requireNonNull(renderingContext);
}
@Override
public <T> Optional<RenderedCriterion> visit(ColumnAndConditionCriterion<T> criterion) {
Optional<FragmentAndParameters> initialCriterion = renderColumnAndCondition(criterion);
List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(criterion.subCriteria());
return initialCriterion.map(fp -> calculateRenderedCriterion(fp, renderedSubCriteria, this::calculateFragment))
.orElseGet(() -> calculateRenderedCriterion(renderedSubCriteria, this::calculateFragment));
}
@Override
public Optional<RenderedCriterion> visit(ExistsCriterion criterion) {
FragmentAndParameters initialCriterion = renderExists(criterion);
List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(criterion.subCriteria());
return calculateRenderedCriterion(initialCriterion, renderedSubCriteria, this::calculateFragment);
}
@Override
public Optional<RenderedCriterion> visit(CriteriaGroup criterion) {
return renderCriteriaGroup(criterion, this::calculateFragment);
}
@Override
public Optional<RenderedCriterion> visit(NotCriterion criterion) {
return renderCriteriaGroup(criterion, this::calculateNotFragment);
}
private Optional<RenderedCriterion> renderCriteriaGroup(CriteriaGroup criterion,
Function<FragmentCollector, String> fragmentCalculator) {
return criterion.initialCriterion().map(ic -> render(ic, criterion.subCriteria(), fragmentCalculator))
.orElseGet(() -> render(criterion.subCriteria(), fragmentCalculator));
}
public Optional<RenderedCriterion> render(SqlCriterion initialCriterion, List<AndOrCriteriaGroup> subCriteria,
Function<FragmentCollector, String> fragmentCalculator) {
Optional<FragmentAndParameters> fragmentAndParameters = initialCriterion.accept(this)
.map(RenderedCriterion::fragmentAndParameters);
List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(subCriteria);
return fragmentAndParameters.map(fp -> calculateRenderedCriterion(fp, renderedSubCriteria, fragmentCalculator))
.orElseGet(() -> calculateRenderedCriterion(renderedSubCriteria, fragmentCalculator));
}
public Optional<RenderedCriterion> render(List<AndOrCriteriaGroup> subCriteria,
Function<FragmentCollector, String> fragmentCalculator) {
List<RenderedCriterion> renderedSubCriteria = renderSubCriteria(subCriteria);
return calculateRenderedCriterion(renderedSubCriteria, fragmentCalculator);
}
private <T> Optional<FragmentAndParameters> renderColumnAndCondition(ColumnAndConditionCriterion<T> criterion) {
if (criterion.condition().shouldRender(renderingContext)) {
return Optional.of(renderCondition(criterion));
} else {
criterion.condition().renderingSkipped();
return Optional.empty();
}
}
private FragmentAndParameters renderExists(ExistsCriterion criterion) {
ExistsPredicate existsPredicate = criterion.existsPredicate();
SelectStatementProvider selectStatement = existsPredicate.selectModelBuilder().build().render(renderingContext);
String fragment = existsPredicate.operator()
+ " (" //$NON-NLS-1$
+ selectStatement.getSelectStatement()
+ ")"; //$NON-NLS-1$
return FragmentAndParameters
.withFragment(fragment)
.withParameters(selectStatement.getParameters())
.build();
}
private List<RenderedCriterion> renderSubCriteria(List<AndOrCriteriaGroup> subCriteria) {
return subCriteria.stream().map(this::renderAndOrCriteriaGroup)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
private Optional<RenderedCriterion> renderAndOrCriteriaGroup(AndOrCriteriaGroup criterion) {
return criterion.initialCriterion().map(ic -> render(ic, criterion.subCriteria(), this::calculateFragment))
.orElseGet(() -> render(criterion.subCriteria(), this::calculateFragment))
.map(rc -> rc.withConnector(criterion.connector()));
}
private Optional<RenderedCriterion> calculateRenderedCriterion(FragmentAndParameters initialCriterion,
List<RenderedCriterion> renderedSubCriteria, Function<FragmentCollector, String> fragmentCalculator) {
return Optional.of(calculateRenderedCriterion(
collectSqlFragments(initialCriterion, renderedSubCriteria), fragmentCalculator));
}
private RenderedCriterion calculateRenderedCriterion(FragmentCollector fragmentCollector,
Function<FragmentCollector, String> fragmentCalculator) {
FragmentAndParameters fragmentAndParameters = FragmentAndParameters
.withFragment(fragmentCalculator.apply(fragmentCollector))
.withParameters(fragmentCollector.parameters())
.build();
return new RenderedCriterion.Builder()
.withFragmentAndParameters(fragmentAndParameters)
.build();
}
private Optional<RenderedCriterion> calculateRenderedCriterion(List<RenderedCriterion> renderedSubCriteria,
Function<FragmentCollector, String> fragmentCalculator) {
return collectSqlFragments(renderedSubCriteria).map(fc -> calculateRenderedCriterion(fc, fragmentCalculator));
}
private <T> FragmentAndParameters renderCondition(ColumnAndConditionCriterion<T> criterion) {
return new ColumnAndConditionRenderer.Builder<T>()
.withColumn(criterion.column())
.withCondition(criterion.condition())
.withRenderingContext(renderingContext)
.build()
.render();
}
/**
* This method encapsulates the logic of building a collection of fragments from an initial condition
* and a list of rendered sub criteria. In this overload we know there is an initial condition
* and there may be subcriteria. The collector will contain the initial condition and any rendered subcriteria
* in order.
*
* @param initialCondition - may not be null. If there is no initial condition, then use the other overload
* @param renderedSubCriteria - a list of previously rendered sub criteria. The sub criteria will all
* have connectors (either an AND or an OR)
* @return a fragment collector whose fragments represent the final calculated list of fragments and parameters.
* The fragment collector can be used to calculate the single composed fragment - either as a where clause, or
* a valid rendered sub criteria in the case of a recursive call.
*/
private FragmentCollector collectSqlFragments(FragmentAndParameters initialCondition,
List<RenderedCriterion> renderedSubCriteria) {
return renderedSubCriteria.stream()
.map(RenderedCriterion::fragmentAndParametersWithConnector)
.collect(FragmentCollector.collect(initialCondition));
}
/**
* This method encapsulates the logic of building a collection of fragments from a list of rendered sub criteria.
* In this overload we take the initial condition to be the first element in the subcriteria list.
* The collector will contain the rendered subcriteria in order. However, the connector from the first rendered
* sub criterion will be removed. This to avoid generating an invalid where clause like "where and a < 3"
*
* @param renderedSubCriteria - a list of previously rendered sub criteria. The sub criteria will all
* have connectors (either an AND or an OR)
* @return a fragment collector whose fragments represent the final calculated list of fragments and parameters.
* The fragment collector can be used to calculate the single composed fragment - either as a where clause, or
* a valid rendered sub criteria in the case of a recursive call.
*/
private Optional<FragmentCollector> collectSqlFragments(List<RenderedCriterion> renderedSubCriteria) {
if (renderedSubCriteria.isEmpty()) {
return Optional.empty();
}
FragmentAndParameters firstCondition = renderedSubCriteria.get(0).fragmentAndParameters();
FragmentCollector fc = renderedSubCriteria.stream()
.skip(1)
.map(RenderedCriterion::fragmentAndParametersWithConnector)
.collect(FragmentCollector.collect(firstCondition));
return Optional.of(fc);
}
private String calculateFragment(FragmentCollector collector) {
if (collector.hasMultipleFragments()) {
return collector.collectFragments(
Collectors.joining(" ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
} else {
return collector.firstFragment().orElse(""); //$NON-NLS-1$
}
}
private String calculateNotFragment(FragmentCollector collector) {
if (collector.hasMultipleFragments()) {
return collector.collectFragments(
Collectors.joining(" ", "not (", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
} else {
return collector.firstFragment().map(s -> "not " + s).orElse(""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}