All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.data.cassandra.core.cql.util.StatementBuilder Maven / Gradle / Ivy

There is a newer version: 4.3.2
Show newest version
/*
 * Copyright 2019-2021 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.springframework.data.cassandra.core.cql.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder;
import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
import com.datastax.oss.driver.api.querybuilder.BuildableQuery;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.datastax.oss.driver.api.querybuilder.term.Term;
import com.datastax.oss.driver.internal.querybuilder.CqlHelper;

import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Functional builder for Cassandra {@link BuildableQuery statements}. Statements are built by applying
 * {@link UnaryOperator builder functions} that get applied when {@link #build() building} the actual
 * {@link SimpleStatement statement}. The {@code StatmentBuilder} provides a mutable container for statement creation
 * allowing a functional declaration of actions that are necessary to build a statement. This class helps building CQL
 * statements as a {@link BuildableQuery} classes are typically immutable and require return value tracking across
 * methods that want to apply modifications to a statement.
 * 

* Building a statement consists of three phases: *

    *
  1. Creation of the {@link StatementBuilder} with a {@link BuildableQuery query stub}
  2. *
  3. Functional declaration applying {@link UnaryOperator builder functions}, {@link BindFunction bind functions} and * {@link Consumer on build signals}
  4. *
  5. Building the statement using {@link #build()}
  6. *
* The initial {@link BuildableQuery query stub} is used as base object for all built queries. Builder functions are * applied each time a statement is built allowing to build multiple statement instances while evolving the actual * statement. *

* The builder can be used for structural evolution and value evolution of statements. Values are bound through * {@link BindFunction binding functions} that accept the statement and a {@link TermFactory}. Values can be bound * inline or through bind markers when {@link #build(ParameterHandling, CodecRegistry) building} the statement. All * functions are applied in the order of their declaration. *

* All methods returning {@link StatementBuilder} point to the same instance. This class is intended for internal use. * * @author Mark Paluch * @param Statement type * @since 3.0 */ public class StatementBuilder { private S statement; private List> queryActions = new ArrayList<>(); private List> onBuild = new ArrayList<>(); private List> onBuilt = new ArrayList<>(); /** * Factory method used to create a new {@link StatementBuilder} with the given {@link BuildableQuery query stub}. * The stub is used as base for the built query so each query inherits properties of this stub. * * @param query type. * @param stub the {@link BuildableQuery query stub} to use. * @return a {@link StatementBuilder} for the given {@link BuildableQuery query stub}. * @throws IllegalArgumentException if the {@link BuildableQuery query stub} is {@literal null}. * @see com.datastax.oss.driver.api.querybuilder.BuildableQuery */ public static StatementBuilder of(S stub) { Assert.notNull(stub, "Query stub must not be null"); return new StatementBuilder<>(stub); } /** * Constructs a new instance of this {@link StatementBuilder} with the given {@link BuildableQuery query stub}. * * @param statement the {@link BuildableQuery query stub} from which to build * the {@link com.datastax.oss.driver.api.core.cql.Statement}. * @see com.datastax.oss.driver.api.querybuilder.BuildableQuery */ private StatementBuilder(S statement) { this.statement = statement; } /** * Apply a {@link BindFunction} to the statement. Bind functions are applied on {@link #build()}. * * @param action the bind function to be applied to the statement. * @return {@code this} {@link StatementBuilder}. */ public StatementBuilder bind(BindFunction action) { Assert.notNull(action, "BindFunction must not be null"); queryActions.add(action::bind); return this; } /** * Apply a {@link UnaryOperator builder function} to the statement. Builder functions are applied on {@link #build()}. * * @param action the builder function to be applied to the statement. * @return {@code this} {@link StatementBuilder}. */ @SuppressWarnings("unchecked") public StatementBuilder apply(Function action) { Assert.notNull(action, "BindFunction must not be null"); queryActions.add((source, termFactory) -> (S) action.apply(source)); return this; } /** * Add behavior when the statement is built. The {@link Consumer} gets invoked with a {@link SimpleStatementBuilder} * allowing association of the final statement with additional settings. The {@link Consumer} is applied on * {@link #build()}. * * @param action the {@link Consumer} function that gets notified on {@link #build()}. * @return {@code this} {@link StatementBuilder}. */ public StatementBuilder onBuild(Consumer action) { Assert.notNull(action, "Consumer must not be null"); onBuild.add(action); return this; } /** * Add behavior after the {@link SimpleStatement} has been built. The {@link UnaryOperator} gets invoked with a * {@link SimpleStatement} allowing association of the final statement with additional settings. The * {@link UnaryOperator} is applied on {@link #build()}. * * @param mappingFunction the {@link UnaryOperator} function that gets notified on {@link #build()}. * @return {@code this} {@link StatementBuilder}. */ public StatementBuilder transform(UnaryOperator mappingFunction) { Assert.notNull(mappingFunction, "Mapping function must not be null"); onBuilt.add(mappingFunction); return this; } /** * Build a {@link SimpleStatement statement} by applying builder and bind functions using the default * {@link CodecRegistry} and {@link ParameterHandling#INLINE} parameter rendering. * * @return the built {@link SimpleStatement}. */ public SimpleStatement build() { return build(ParameterHandling.INLINE, CodecRegistry.DEFAULT); } /** * Build a {@link SimpleStatement statement} by applying builder and bind functions using the given * {@link ParameterHandling}. * * @param parameterHandling {@link ParameterHandling} used to determine how to render parameters. * @return the built {@link SimpleStatement}. */ public SimpleStatement build(ParameterHandling parameterHandling) { return build(parameterHandling, CodecRegistry.DEFAULT); } /** * Build a {@link SimpleStatement statement} by applying builder and bind functions using the given * {@link CodecRegistry} and {@link ParameterHandling}. * * @param parameterHandling {@link ParameterHandling} used to determine how to render parameters. * @param codecRegistry registry of Apache Cassandra codecs for converting to/from Java types and CQL types. * @return the built {@link SimpleStatement}. */ public SimpleStatement build(ParameterHandling parameterHandling, CodecRegistry codecRegistry) { Assert.notNull(parameterHandling, "ParameterHandling must not be null"); Assert.notNull(codecRegistry, "CodecRegistry must not be null"); S statement = this.statement; if (parameterHandling == ParameterHandling.INLINE) { TermFactory termFactory = value -> toLiteralTerms(value, codecRegistry); for (BuilderRunnable runnable : queryActions) { statement = runnable.run(statement, termFactory); } return build(statement.builder()); } if (parameterHandling == ParameterHandling.BY_INDEX) { List values = new ArrayList<>(); TermFactory termFactory = value -> { values.add(value); return QueryBuilder.bindMarker(); }; for (BuilderRunnable runnable : queryActions) { statement = runnable.run(statement, termFactory); } return build(statement.builder().addPositionalValues(values)); } if (parameterHandling == ParameterHandling.BY_NAME) { Map values = new LinkedHashMap<>(); TermFactory termFactory = value -> { String name = "p" + values.size(); values.put(name, value); return QueryBuilder.bindMarker(name); }; for (BuilderRunnable runnable : queryActions) { statement = runnable.run(statement, termFactory); } SimpleStatementBuilder builder = statement.builder(); values.forEach(builder::addNamedValue); return build(builder); } throw new UnsupportedOperationException(String.format("ParameterHandling %s not supported", parameterHandling)); } private SimpleStatement build(SimpleStatementBuilder builder) { SimpleStatement statmentToUse = onBuild(builder).build(); for (UnaryOperator operator : onBuilt) { statmentToUse = operator.apply(statmentToUse); } return statmentToUse; } private SimpleStatementBuilder onBuild(SimpleStatementBuilder statementBuilder) { onBuild.forEach(it -> it.accept(statementBuilder)); return statementBuilder; } @SuppressWarnings("unchecked") private static Term toLiteralTerms(@Nullable Object value, CodecRegistry codecRegistry) { if (value instanceof List) { List terms = new ArrayList<>(); for (Object o : (List) value) { terms.add(toLiteralTerms(o, codecRegistry)); } return new ListTerm(terms); } if (value instanceof Set) { List terms = new ArrayList<>(); for (Object o : (Set) value) { terms.add(toLiteralTerms(o, codecRegistry)); } return new SetTerm(terms); } if (value instanceof Map) { Map terms = new LinkedHashMap<>(); ((Map) value).forEach((k, v) -> terms.put(toLiteralTerms(k, codecRegistry), toLiteralTerms(v, codecRegistry))); return new MapTerm(terms); } return QueryBuilder.literal(value, codecRegistry); } /** * Binding function. This function gets called with the current statement and {@link TermFactory}. * * @param */ @FunctionalInterface public interface BindFunction { /** * Apply a binding operation on the {@link BuildableQuery statement} and return the modified statement instance. * * @param statement the initial statement instance. * @param factory factory to create {@link com.datastax.oss.driver.api.querybuilder.term.Term} objects. * @return the modified statement instance. */ S bind(S statement, TermFactory factory); } @FunctionalInterface interface BuilderRunnable { S run(S source, TermFactory termFactory); } /** * Enumeration to represent how parameters are rendered. */ public enum ParameterHandling { /** * CQL inline rendering as literals. */ INLINE, /** * Index-based bind markers. */ BY_INDEX, /** * Named bind markers. */ BY_NAME } static class ListTerm implements Term { private final Collection components; public ListTerm(@NonNull Collection components) { this.components = components; } @Override public void appendTo(@NonNull StringBuilder builder) { if (components.isEmpty()) { builder.append("[]"); return; } CqlHelper.append(components, builder, "[", ",", "]"); } @Override public boolean isIdempotent() { for (Term component : components) { if (!component.isIdempotent()) { return false; } } return true; } } static class SetTerm implements Term { private final Collection components; public SetTerm(@NonNull Collection components) { this.components = components; } @Override public void appendTo(@NonNull StringBuilder builder) { if (components.isEmpty()) { builder.append("{}"); return; } CqlHelper.append(components, builder, "{", ",", "}"); } @Override public boolean isIdempotent() { for (Term component : components) { if (!component.isIdempotent()) { return false; } } return true; } } static class MapTerm implements Term { private final Map components; public MapTerm(Map components) { this.components = components; } @Override @SuppressWarnings("all") public void appendTo(@NonNull StringBuilder builder) { if (components.isEmpty()) { builder.append("{}"); return; } boolean first = true; for (Map.Entry entry : components.entrySet()) { if (first) { builder.append("{"); first = false; } else { builder.append(","); } entry.getKey().appendTo(builder); builder.append(":"); entry.getValue().appendTo(builder); } if (!first) { builder.append("}"); } } @Override public boolean isIdempotent() { for (Map.Entry entry : components.entrySet()) { if (!entry.getKey().isIdempotent() || !entry.getValue().isIdempotent()) { return false; } } return true; } } }