/*
* Copyright (C) 2012-2015 DataStax Inc.
*
* 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
*
* http://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 com.datastax.driver.core.querybuilder;
import com.datastax.driver.core.*;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import com.datastax.driver.core.policies.RetryPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Common ancestor to statements generated with the {@link QueryBuilder}.
* <p/>
* The actual query string will be generated and cached the first time it is requested,
* which is either when the driver tries to execute the query, or when you call certain
* public methods (for example {@link RegularStatement#getQueryString(CodecRegistry)},
* {@link #getObject(int, CodecRegistry)}).
* <p/>
* Whenever possible (and unless you call {@link #setForceNoValues(boolean)}, the builder
* will try to handle values passed to its methods as standalone values bound to the query
* string with placeholders. For instance:
* <pre>
* select().all().from("foo").where(eq("k", "the key"));
* // Is equivalent to:
* new SimpleStatement("SELECT * FROM foo WHERE k=?", "the key");
* </pre>
* There are a few exceptions to this rule:
* <ul>
* <li>for fixed-size number types, the builder can't guess what the actual CQL type
* is. Standalone values are sent to Cassandra in their serialized form, and number
* types aren't all serialized in the same way, so picking the wrong type could
* lead to a query error;</li>
* <li>if the value is a "special" element like a function call, it can't be serialized.
* This also applies to collections mixing special elements and regular objects.</li>
* </ul>
* In these cases, the builder will inline the value in the query string:
* <pre>
* select().all().from("foo").where(eq("k", 1));
* // Is equivalent to:
* new SimpleStatement("SELECT * FROM foo WHERE k=1");
* </pre>
* One final thing to consider is {@link CodecRegistry custom codecs}. If you've registered
* codecs to handle your own Java types against the cluster, then you can pass instances of
* those types to query builder methods. But should the builder have to inline those values,
* it needs your codecs to {@link TypeCodec#format(Object) convert them to string form}.
* That is why some of the public methods of this class take a {@code CodecRegistry} as a
* parameter:
* <pre>
* BuiltStatement s = select().all().from("foo").where(eq("k", myCustomObject));
* // if we do this codecs will definitely be needed:
* s.forceNoValues(true);
* s.getQueryString(myCodecRegistry);
* </pre>
* For convenience, there are no-arg versions of those methods that use
* {@link CodecRegistry#DEFAULT_INSTANCE}. But you should only use them if you are sure that
* no custom values will need to be inlined while building the statement, or if you have
* registered your custom codecs with the default registry instance. Otherwise, you will get
* a {@link CodecNotFoundException}.
*/
public abstract class BuiltStatement extends RegularStatement {
private static final Pattern lowercaseAlphanumeric = Pattern.compile("[a-z][a-z0-9_]*");
private final List<ColumnMetadata> partitionKey;
private final List<Object> routingKeyValues;
final String keyspace;
private boolean dirty;
private String cache;
private List<Object> values;
Boolean isCounterOp;
boolean hasNonIdempotentOps;
// Whether the user has inputted bind markers. If that's the case, we never generate values as
// it means the user meant for the statement to be prepared and we shouldn't add our own markers.
boolean hasBindMarkers;
private boolean forceNoValues;
BuiltStatement(String keyspace, List<ColumnMetadata> partitionKey, List<Object> routingKeyValues) {
this.partitionKey = partitionKey;
this.routingKeyValues = routingKeyValues;
this.keyspace = keyspace;
}
/**
* @deprecated preserved for backward compatibility, use {@link Metadata#quoteIfNecessary(String)} instead.
*/
@Deprecated
protected static String escapeId(String ident) {
return Metadata.quoteIfNecessary(ident);
}
@Override
public String getQueryString(CodecRegistry codecRegistry) {
maybeRebuildCache(codecRegistry);
return cache;
}
/**
* Returns the {@code i}th value as the Java type matching its CQL type.
*
* @param i the index to retrieve.
* @param codecRegistry the codec registry that will be used if the statement must be
* rebuilt in order to determine if it has values, and Java objects
* must be inlined in the process (see {@link BuiltStatement} for
* more explanations on why this is so).
* @return the value of the {@code i}th value of this statement.
* @throws IllegalStateException if this statement does not have values.
* @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object.
* @see #getObject(int)
*/
public Object getObject(int i, CodecRegistry codecRegistry) {
maybeRebuildCache(codecRegistry);
if (values == null || values.isEmpty())
throw new IllegalStateException("This statement does not have values");
if (i < 0 || i >= values.size())
throw new ArrayIndexOutOfBoundsException(i);
return values.get(i);
}
/**
* Returns the {@code i}th value as the Java type matching its CQL type.
* <p/>
* This method calls {@link #getObject(int, CodecRegistry)} with
* {@link CodecRegistry#DEFAULT_INSTANCE}.
* It's safe to use if you don't use any custom codecs, or if your custom codecs are in
* the default registry; otherwise, use the other method and provide the registry that
* contains your codecs.
*
* @param i the index to retrieve.
* @return the value of the {@code i}th value of this statement.
* @throws IllegalStateException if this statement does not have values.
* @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object.
*/
public Object getObject(int i) {
return getObject(i, CodecRegistry.DEFAULT_INSTANCE);
}
private void maybeRebuildCache(CodecRegistry codecRegistry) {
if (!dirty && cache != null)
return;
StringBuilder sb;
values = null;
if (hasBindMarkers || forceNoValues) {
sb = buildQueryString(null, codecRegistry);
} else {
values = new ArrayList<Object>();
sb = buildQueryString(values, codecRegistry);
if (values.size() > 65535)
throw new IllegalArgumentException("Too many values for built statement, the maximum allowed is 65535");
if (values.isEmpty())
values = null;
}
maybeAddSemicolon(sb);
cache = sb.toString();
dirty = false;
}
static StringBuilder maybeAddSemicolon(StringBuilder sb) {
// Use the same test that String#trim() uses to determine
// if a character is a whitespace character.
int l = sb.length();
while (l > 0 && sb.charAt(l - 1) <= ' ')
l -= 1;
if (l != sb.length())
sb.setLength(l);
if (l == 0 || sb.charAt(l - 1) != ';')
sb.append(';');
return sb;
}
abstract StringBuilder buildQueryString(List<Object> variables, CodecRegistry codecRegistry);
boolean isCounterOp() {
return isCounterOp == null ? false : isCounterOp;
}
void setCounterOp(boolean isCounterOp) {
this.isCounterOp = isCounterOp;
}
boolean hasNonIdempotentOps() {
return hasNonIdempotentOps;
}
void setNonIdempotentOps() {
hasNonIdempotentOps = true;
}
void setDirty() {
dirty = true;
}
void checkForBindMarkers(Object value) {
dirty = true;
if (Utils.containsBindMarker(value))
hasBindMarkers = true;
}
void checkForBindMarkers(Utils.Appendeable value) {
dirty = true;
if (value != null && value.containsBindMarker())
hasBindMarkers = true;
}
// TODO: Correctly document the InvalidTypeException
void maybeAddRoutingKey(String name, Object value) {
if (routingKeyValues == null || name == null || value == null || Utils.containsSpecialValue(value))
return;
for (int i = 0; i < partitionKey.size(); i++) {
if (Utils.handleId(name).equals(partitionKey.get(i).getName())) {
routingKeyValues.set(i, value);
return;
}
}
}
@Override
public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
if (routingKeyValues == null)
return null;
ByteBuffer[] routingKeyParts = new ByteBuffer[partitionKey.size()];
for (int i = 0; i < partitionKey.size(); i++) {
Object value = routingKeyValues.get(i);
if (value == null)
return null;
TypeCodec<Object> codec = codecRegistry.codecFor(partitionKey.get(i).getType(), value);
routingKeyParts[i] = codec.serialize(value, protocolVersion);
}
return routingKeyParts.length == 1
? routingKeyParts[0]
: Utils.compose(routingKeyParts);
}
@Override
public String getKeyspace() {
return keyspace;
}
@Override
public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
maybeRebuildCache(codecRegistry);
return values == null ? null : Utils.convert(values.toArray(), protocolVersion, codecRegistry);
}
@Override
public boolean hasValues(CodecRegistry codecRegistry) {
maybeRebuildCache(codecRegistry);
return values != null;
}
@Override
public Map<String, ByteBuffer> getNamedValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
// Built statements never return named values
return null;
}
@Override
public boolean usesNamedValues() {
return false;
}
@Override
public Boolean isIdempotent() {
// If a value was forced with setIdempotent, it takes priority
if (idempotent != null)
return idempotent;
// Otherwise return the computed value
return !hasNonIdempotentOps();
}
@Override
public String toString() {
try {
if (forceNoValues)
return getQueryString();
// 1) try first with all values inlined (will not work if some values require custom codecs,
// or if the required codecs are registered in a different CodecRegistry instance than the default one)
return maybeAddSemicolon(buildQueryString(null, CodecRegistry.DEFAULT_INSTANCE)).toString();
} catch (RuntimeException e1) {
// 2) try next with bind markers for all values to avoid usage of custom codecs
try {
return maybeAddSemicolon(buildQueryString(new ArrayList<Object>(), CodecRegistry.DEFAULT_INSTANCE)).toString();
} catch (RuntimeException e2) {
// Ugly but we have absolutely no context to get the registry from
return String.format("built query (could not generate with default codec registry: %s)", e2.getMessage());
}
}
}
/**
* Allows to force this builder to not generate values (through its {@code getValues()} method).
* <p/>
* By default (and unless the protocol version 1 is in use, see below) and
* for performance reasons, the query builder will not serialize all values
* provided to strings. This means that {@link #getQueryString(CodecRegistry)}
* may return a query string with bind markers (where and when is at the
* discretion of the builder) and {@link #getValues} will return the binary
* values for those markers. This method allows to force the builder to not
* generate binary values but rather to inline them all in the query
* string. In practice, this means that if you call {@code
* setForceNoValues(true)}, you are guaranteed that {@code getValues()} will
* return {@code null} and that the string returned by {@code
* getQueryString()} will contain no other bind markers than the ones
* specified by the user.
* <p/>
* If the native protocol version 1 is in use, the driver will default
* to not generating values since those are not supported by that version of
* the protocol. In practice, the driver will automatically call this method
* with {@code true} as argument prior to execution. Hence, calling this
* method when the protocol version 1 is in use is basically a no-op.
* <p/>
* Note that this method is mainly useful for debugging purpose. In general,
* the default behavior should be the correct and most efficient one.
*
* @param forceNoValues whether or not this builder may generate values.
* @return this statement.
*/
public RegularStatement setForceNoValues(boolean forceNoValues) {
this.forceNoValues = forceNoValues;
this.dirty = true;
return this;
}
/**
* An utility class to create a BuiltStatement that encapsulate another one.
*/
abstract static class ForwardingStatement<T extends BuiltStatement> extends BuiltStatement {
T statement;
ForwardingStatement(T statement) {
super(null, null, null);
this.statement = statement;
}
@Override
public String getQueryString(CodecRegistry codecRegistry) {
return statement.getQueryString(codecRegistry);
}
@Override
StringBuilder buildQueryString(List<Object> values, CodecRegistry codecRegistry) {
return statement.buildQueryString(values, codecRegistry);
}
@Override
public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
return statement.getRoutingKey(protocolVersion, codecRegistry);
}
@Override
public String getKeyspace() {
return statement.getKeyspace();
}
@Override
boolean isCounterOp() {
return statement.isCounterOp();
}
@Override
boolean hasNonIdempotentOps() {
return statement.hasNonIdempotentOps();
}
@Override
public RegularStatement setForceNoValues(boolean forceNoValues) {
statement.setForceNoValues(forceNoValues);
return this;
}
@Override
public Statement setConsistencyLevel(ConsistencyLevel consistency) {
statement.setConsistencyLevel(consistency);
return this;
}
@Override
public ConsistencyLevel getConsistencyLevel() {
return statement.getConsistencyLevel();
}
@Override
public Statement enableTracing() {
statement.enableTracing();
return this;
}
@Override
public Statement disableTracing() {
statement.disableTracing();
return this;
}
@Override
public boolean isTracing() {
return statement.isTracing();
}
@Override
public Statement setRetryPolicy(RetryPolicy policy) {
statement.setRetryPolicy(policy);
return this;
}
@Override
public RetryPolicy getRetryPolicy() {
return statement.getRetryPolicy();
}
@Override
public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
return statement.getValues(protocolVersion, codecRegistry);
}
@Override
public boolean hasValues() {
return statement.hasValues();
}
@Override
void checkForBindMarkers(Object value) {
statement.checkForBindMarkers(value);
}
@Override
void checkForBindMarkers(Utils.Appendeable value) {
statement.checkForBindMarkers(value);
}
@Override
public String toString() {
return statement.toString();
}
}
}