com.datastax.oss.driver.internal.core.type.codec.registry.CachingCodecRegistry Maven / Gradle / Ivy
/*
* Copyright 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.oss.driver.internal.core.type.codec.registry;
import com.datastax.oss.driver.api.core.data.CqlDuration;
import com.datastax.oss.driver.api.core.data.CqlVector;
import com.datastax.oss.driver.api.core.data.TupleValue;
import com.datastax.oss.driver.api.core.data.UdtValue;
import com.datastax.oss.driver.api.core.type.*;
import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException;
import com.datastax.oss.driver.api.core.type.codec.TypeCodec;
import com.datastax.oss.driver.api.core.type.codec.TypeCodecs;
import com.datastax.oss.driver.api.core.type.codec.registry.MutableCodecRegistry;
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
import com.datastax.oss.driver.shaded.guava.common.base.Preconditions;
import com.datastax.oss.driver.shaded.guava.common.reflect.TypeToken;
import com.datastax.oss.protocol.internal.ProtocolConstants;
import com.datastax.oss.protocol.internal.util.IntMap;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.jcip.annotations.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A codec registry that handles built-in type mappings, can be extended with a list of
* user-provided codecs, generates more complex codecs from those basic codecs, and caches generated
* codecs for reuse.
*
* The primitive mappings always take precedence over any user codec. The list of user codecs can
* not be modified after construction.
*
*
This class is abstract in order to be agnostic from the cache implementation. Subclasses must
* implement {@link #getCachedCodec(DataType, GenericType, boolean)}.
*/
@ThreadSafe
public abstract class CachingCodecRegistry implements MutableCodecRegistry {
private static final Logger LOG = LoggerFactory.getLogger(CachingCodecRegistry.class);
// Implementation notes:
// - built-in primitive codecs are served directly, without hitting the cache
// - same for user codecs (we assume the cardinality will always be low, so a sequential array
// traversal is cheap).
protected final String logPrefix;
private final TypeCodec[] primitiveCodecs;
private final CopyOnWriteArrayList> userCodecs = new CopyOnWriteArrayList<>();
private final IntMap> primitiveCodecsByCode;
private final Lock registerLock = new ReentrantLock();
protected CachingCodecRegistry(
@NonNull String logPrefix, @NonNull TypeCodec[] primitiveCodecs) {
this.logPrefix = logPrefix;
this.primitiveCodecs = primitiveCodecs;
this.primitiveCodecsByCode = sortByProtocolCode(primitiveCodecs);
}
/**
* @deprecated this constructor calls an overridable method ({@link #register(TypeCodec[])}),
* which is a bad practice. The recommended alternative is to use {@link
* #CachingCodecRegistry(String, TypeCodec[])}, then add the codecs with one of the {@link
* #register} methods.
*/
@Deprecated
protected CachingCodecRegistry(
@NonNull String logPrefix,
@NonNull TypeCodec[] primitiveCodecs,
@NonNull TypeCodec[] userCodecs) {
this(logPrefix, primitiveCodecs);
register(userCodecs);
}
@Override
public void register(TypeCodec newCodec) {
// This method could work without synchronization, but there is a tiny race condition that would
// allow two threads to register colliding codecs (the last added codec would later be ignored,
// but without any warning). Serialize calls to avoid that:
registerLock.lock();
try {
for (TypeCodec primitiveCodec : primitiveCodecs) {
if (collides(newCodec, primitiveCodec)) {
LOG.warn(
"[{}] Ignoring codec {} because it collides with built-in primitive codec {}",
logPrefix,
newCodec,
primitiveCodec);
return;
}
}
for (TypeCodec userCodec : userCodecs) {
if (collides(newCodec, userCodec)) {
LOG.warn(
"[{}] Ignoring codec {} because it collides with previously registered codec {}",
logPrefix,
newCodec,
userCodec);
return;
}
}
// Technically this would cover the two previous cases as well, but we want precise messages.
try {
TypeCodec cachedCodec =
getCachedCodec(newCodec.getCqlType(), newCodec.getJavaType(), false);
LOG.warn(
"[{}] Ignoring codec {} because it collides with previously generated codec {}",
logPrefix,
newCodec,
cachedCodec);
return;
} catch (CodecNotFoundException ignored) {
// Catching the exception is ugly, but it avoids breaking the internal API (e.g. by adding a
// getCachedCodecIfExists)
}
userCodecs.add(newCodec);
} finally {
registerLock.unlock();
}
}
private boolean collides(TypeCodec newCodec, TypeCodec oldCodec) {
return oldCodec.accepts(newCodec.getCqlType()) && oldCodec.accepts(newCodec.getJavaType());
}
/**
* Gets a complex codec from the cache.
*
* If the codec does not exist in the cache, this method must generate it with {@link
* #createCodec(DataType, GenericType, boolean)} (and most likely put it in the cache too for
* future calls).
*/
protected abstract TypeCodec getCachedCodec(
@Nullable DataType cqlType, @Nullable GenericType javaType, boolean isJavaCovariant);
@NonNull
@Override
public TypeCodec codecFor(
@NonNull DataType cqlType, @NonNull GenericType javaType) {
return codecFor(cqlType, javaType, false);
}
// Not exposed publicly, (isJavaCovariant=true) is only used for internal recursion
@NonNull
protected TypeCodec codecFor(
@NonNull DataType cqlType,
@NonNull GenericType javaType,
boolean isJavaCovariant) {
LOG.trace("[{}] Looking up codec for {} <-> {}", logPrefix, cqlType, javaType);
TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode());
if (primitiveCodec != null && matches(primitiveCodec, javaType, isJavaCovariant)) {
LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec);
return uncheckedCast(primitiveCodec);
}
for (TypeCodec userCodec : userCodecs) {
if (userCodec.accepts(cqlType) && matches(userCodec, javaType, isJavaCovariant)) {
LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec);
return uncheckedCast(userCodec);
}
}
return uncheckedCast(getCachedCodec(cqlType, javaType, isJavaCovariant));
}
@NonNull
@Override
public TypeCodec codecFor(
@NonNull DataType cqlType, @NonNull Class javaType) {
LOG.trace("[{}] Looking up codec for {} <-> {}", logPrefix, cqlType, javaType);
TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode());
if (primitiveCodec != null && primitiveCodec.accepts(javaType)) {
LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec);
return uncheckedCast(primitiveCodec);
}
for (TypeCodec userCodec : userCodecs) {
if (userCodec.accepts(cqlType) && userCodec.accepts(javaType)) {
LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec);
return uncheckedCast(userCodec);
}
}
return uncheckedCast(getCachedCodec(cqlType, GenericType.of(javaType), false));
}
@NonNull
@Override
public TypeCodec codecFor(@NonNull DataType cqlType) {
LOG.trace("[{}] Looking up codec for CQL type {}", logPrefix, cqlType);
TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode());
if (primitiveCodec != null) {
LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec);
return uncheckedCast(primitiveCodec);
}
for (TypeCodec userCodec : userCodecs) {
if (userCodec.accepts(cqlType)) {
LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec);
return uncheckedCast(userCodec);
}
}
return uncheckedCast(getCachedCodec(cqlType, null, false));
}
@NonNull
@Override
public TypeCodec codecFor(
@NonNull DataType cqlType, @NonNull JavaTypeT value) {
Preconditions.checkNotNull(cqlType);
Preconditions.checkNotNull(value);
LOG.trace("[{}] Looking up codec for CQL type {} and object {}", logPrefix, cqlType, value);
TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode());
if (primitiveCodec != null && primitiveCodec.accepts(value)) {
LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec);
return uncheckedCast(primitiveCodec);
}
for (TypeCodec userCodec : userCodecs) {
if (userCodec.accepts(cqlType) && userCodec.accepts(value)) {
LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec);
return uncheckedCast(userCodec);
}
}
GenericType javaType = inspectType(value, cqlType);
LOG.trace("[{}] Continuing based on inferred type {}", logPrefix, javaType);
return uncheckedCast(getCachedCodec(cqlType, javaType, true));
}
@NonNull
@Override
public TypeCodec codecFor(@NonNull JavaTypeT value) {
Preconditions.checkNotNull(value);
LOG.trace("[{}] Looking up codec for object {}", logPrefix, value);
for (TypeCodec primitiveCodec : primitiveCodecs) {
if (primitiveCodec.accepts(value)) {
LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec);
return uncheckedCast(primitiveCodec);
}
}
for (TypeCodec userCodec : userCodecs) {
if (userCodec.accepts(value)) {
LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec);
return uncheckedCast(userCodec);
}
}
DataType cqlType = inferCqlTypeFromValue(value);
GenericType javaType = inspectType(value, cqlType);
LOG.trace(
"[{}] Continuing based on inferred CQL type {} and Java type {}",
logPrefix,
cqlType,
javaType);
return uncheckedCast(getCachedCodec(cqlType, javaType, true));
}
@NonNull
@Override
public TypeCodec codecFor(@NonNull GenericType javaType) {
return codecFor(javaType, false);
}
// Not exposed publicly, (isJavaCovariant=true) is only used for internal recursion
@NonNull
protected TypeCodec codecFor(
@NonNull GenericType javaType, boolean isJavaCovariant) {
LOG.trace(
"[{}] Looking up codec for Java type {} (covariant = {})",
logPrefix,
javaType,
isJavaCovariant);
for (TypeCodec primitiveCodec : primitiveCodecs) {
if (matches(primitiveCodec, javaType, isJavaCovariant)) {
LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec);
return uncheckedCast(primitiveCodec);
}
}
for (TypeCodec userCodec : userCodecs) {
if (matches(userCodec, javaType, isJavaCovariant)) {
LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec);
return uncheckedCast(userCodec);
}
}
return uncheckedCast(getCachedCodec(null, javaType, isJavaCovariant));
}
protected boolean matches(
@NonNull TypeCodec codec, @NonNull GenericType javaType, boolean isJavaCovariant) {
return isJavaCovariant ? codec.getJavaType().isSupertypeOf(javaType) : codec.accepts(javaType);
}
@NonNull
protected GenericType inspectType(@NonNull Object value, @Nullable DataType cqlType) {
if (value instanceof List) {
List list = (List) value;
if (list.isEmpty()) {
// Empty collections are always encoded the same way, so any element type will do
// in the absence of a CQL type. When the CQL type is known, we try to infer the best Java
// type.
return cqlType == null ? JAVA_TYPE_FOR_EMPTY_LISTS : inferJavaTypeFromCqlType(cqlType);
} else {
Object firstElement = list.get(0);
if (firstElement == null) {
throw new IllegalArgumentException(
"Can't infer list codec because the first element is null "
+ "(note that CQL does not allow null values in collections)");
}
GenericType elementType =
inspectType(
firstElement, cqlType == null ? null : ((ContainerType) cqlType).getElementType());
return GenericType.listOf(elementType);
}
} else if (value instanceof Set) {
Set set = (Set) value;
if (set.isEmpty()) {
return cqlType == null ? JAVA_TYPE_FOR_EMPTY_SETS : inferJavaTypeFromCqlType(cqlType);
} else {
Object firstElement = set.iterator().next();
if (firstElement == null) {
throw new IllegalArgumentException(
"Can't infer set codec because the first element is null "
+ "(note that CQL does not allow null values in collections)");
}
GenericType elementType =
inspectType(
firstElement, cqlType == null ? null : ((SetType) cqlType).getElementType());
return GenericType.setOf(elementType);
}
} else if (value instanceof Map) {
Map map = (Map) value;
if (map.isEmpty()) {
return cqlType == null ? JAVA_TYPE_FOR_EMPTY_MAPS : inferJavaTypeFromCqlType(cqlType);
} else {
Map.Entry firstEntry = map.entrySet().iterator().next();
Object firstKey = firstEntry.getKey();
Object firstValue = firstEntry.getValue();
if (firstKey == null || firstValue == null) {
throw new IllegalArgumentException(
"Can't infer map codec because the first key and/or value is null "
+ "(note that CQL does not allow null values in collections)");
}
GenericType keyType =
inspectType(firstKey, cqlType == null ? null : ((MapType) cqlType).getKeyType());
GenericType valueType =
inspectType(firstValue, cqlType == null ? null : ((MapType) cqlType).getValueType());
return GenericType.mapOf(keyType, valueType);
}
} else if (value instanceof CqlVector) {
CqlVector vector = (CqlVector) value;
if (vector.isEmpty()) {
return cqlType == null ? JAVA_TYPE_FOR_EMPTY_CQLVECTORS : inferJavaTypeFromCqlType(cqlType);
} else {
Object firstElement = vector.iterator().next();
if (firstElement == null) {
throw new IllegalArgumentException(
"Can't infer vector codec because the first element is null "
+ "(note that CQL does not allow null values in collections)");
}
GenericType elementType =
(GenericType)
inspectType(
firstElement, cqlType == null ? null : ((VectorType) cqlType).getElementType());
return GenericType.vectorOf(elementType);
}
} else {
// There's not much more we can do
return GenericType.of(value.getClass());
}
}
@NonNull
protected GenericType inferJavaTypeFromCqlType(@NonNull DataType cqlType) {
if (cqlType instanceof ListType) {
DataType elementType = ((ListType) cqlType).getElementType();
return GenericType.listOf(inferJavaTypeFromCqlType(elementType));
} else if (cqlType instanceof SetType) {
DataType elementType = ((SetType) cqlType).getElementType();
return GenericType.setOf(inferJavaTypeFromCqlType(elementType));
} else if (cqlType instanceof MapType) {
DataType keyType = ((MapType) cqlType).getKeyType();
DataType valueType = ((MapType) cqlType).getValueType();
return GenericType.mapOf(
inferJavaTypeFromCqlType(keyType), inferJavaTypeFromCqlType(valueType));
} else if (cqlType instanceof VectorType) {
DataType elementType = ((VectorType) cqlType).getElementType();
GenericType numberType =
(GenericType) inferJavaTypeFromCqlType(elementType);
return GenericType.vectorOf(numberType);
}
switch (cqlType.getProtocolCode()) {
case ProtocolConstants.DataType.CUSTOM:
case ProtocolConstants.DataType.BLOB:
return GenericType.BYTE_BUFFER;
case ProtocolConstants.DataType.ASCII:
case ProtocolConstants.DataType.VARCHAR:
return GenericType.STRING;
case ProtocolConstants.DataType.BIGINT:
case ProtocolConstants.DataType.COUNTER:
return GenericType.LONG;
case ProtocolConstants.DataType.BOOLEAN:
return GenericType.BOOLEAN;
case ProtocolConstants.DataType.DECIMAL:
return GenericType.BIG_DECIMAL;
case ProtocolConstants.DataType.DOUBLE:
return GenericType.DOUBLE;
case ProtocolConstants.DataType.FLOAT:
return GenericType.FLOAT;
case ProtocolConstants.DataType.INT:
return GenericType.INTEGER;
case ProtocolConstants.DataType.TIMESTAMP:
return GenericType.INSTANT;
case ProtocolConstants.DataType.UUID:
case ProtocolConstants.DataType.TIMEUUID:
return GenericType.UUID;
case ProtocolConstants.DataType.VARINT:
return GenericType.BIG_INTEGER;
case ProtocolConstants.DataType.INET:
return GenericType.INET_ADDRESS;
case ProtocolConstants.DataType.DATE:
return GenericType.LOCAL_DATE;
case ProtocolConstants.DataType.TIME:
return GenericType.LOCAL_TIME;
case ProtocolConstants.DataType.SMALLINT:
return GenericType.SHORT;
case ProtocolConstants.DataType.TINYINT:
return GenericType.BYTE;
case ProtocolConstants.DataType.DURATION:
return GenericType.CQL_DURATION;
case ProtocolConstants.DataType.UDT:
return GenericType.UDT_VALUE;
case ProtocolConstants.DataType.TUPLE:
return GenericType.TUPLE_VALUE;
default:
throw new CodecNotFoundException(cqlType, null);
}
}
@Nullable
protected DataType inferCqlTypeFromValue(@NonNull Object value) {
if (value instanceof List) {
List list = (List) value;
if (list.isEmpty()) {
return CQL_TYPE_FOR_EMPTY_LISTS;
}
Object firstElement = list.get(0);
if (firstElement == null) {
throw new IllegalArgumentException(
"Can't infer list codec because the first element is null "
+ "(note that CQL does not allow null values in collections)");
}
DataType elementType = inferCqlTypeFromValue(firstElement);
if (elementType == null) {
return null;
}
return DataTypes.listOf(elementType);
} else if (value instanceof Set) {
Set set = (Set) value;
if (set.isEmpty()) {
return CQL_TYPE_FOR_EMPTY_SETS;
}
Object firstElement = set.iterator().next();
if (firstElement == null) {
throw new IllegalArgumentException(
"Can't infer set codec because the first element is null "
+ "(note that CQL does not allow null values in collections)");
}
DataType elementType = inferCqlTypeFromValue(firstElement);
if (elementType == null) {
return null;
}
return DataTypes.setOf(elementType);
} else if (value instanceof Map) {
Map map = (Map) value;
if (map.isEmpty()) {
return CQL_TYPE_FOR_EMPTY_MAPS;
}
Entry firstEntry = map.entrySet().iterator().next();
Object firstKey = firstEntry.getKey();
Object firstValue = firstEntry.getValue();
if (firstKey == null || firstValue == null) {
throw new IllegalArgumentException(
"Can't infer map codec because the first key and/or value is null "
+ "(note that CQL does not allow null values in collections)");
}
DataType keyType = inferCqlTypeFromValue(firstKey);
DataType valueType = inferCqlTypeFromValue(firstValue);
if (keyType == null || valueType == null) {
return null;
}
return DataTypes.mapOf(keyType, valueType);
} else if (value instanceof CqlVector) {
CqlVector vector = (CqlVector) value;
if (vector.isEmpty()) {
return CQL_TYPE_FOR_EMPTY_VECTORS;
}
Object firstElement = vector.iterator().next();
if (firstElement == null) {
throw new IllegalArgumentException(
"Can't infer vector codec because the first element is null "
+ "(note that CQL does not allow null values in collections)");
}
DataType elementType = inferCqlTypeFromValue(firstElement);
if (elementType == null) {
return null;
}
return DataTypes.vectorOf(elementType, vector.size());
}
Class javaClass = value.getClass();
if (ByteBuffer.class.isAssignableFrom(javaClass)) {
return DataTypes.BLOB;
} else if (String.class.equals(javaClass)) {
return DataTypes.TEXT;
} else if (Long.class.equals(javaClass)) {
return DataTypes.BIGINT;
} else if (Boolean.class.equals(javaClass)) {
return DataTypes.BOOLEAN;
} else if (BigDecimal.class.equals(javaClass)) {
return DataTypes.DECIMAL;
} else if (Double.class.equals(javaClass)) {
return DataTypes.DOUBLE;
} else if (Float.class.equals(javaClass)) {
return DataTypes.FLOAT;
} else if (Integer.class.equals(javaClass)) {
return DataTypes.INT;
} else if (Instant.class.equals(javaClass)) {
return DataTypes.TIMESTAMP;
} else if (UUID.class.equals(javaClass)) {
return DataTypes.UUID;
} else if (BigInteger.class.equals(javaClass)) {
return DataTypes.VARINT;
} else if (InetAddress.class.isAssignableFrom(javaClass)) {
return DataTypes.INET;
} else if (LocalDate.class.equals(javaClass)) {
return DataTypes.DATE;
} else if (LocalTime.class.equals(javaClass)) {
return DataTypes.TIME;
} else if (Short.class.equals(javaClass)) {
return DataTypes.SMALLINT;
} else if (Byte.class.equals(javaClass)) {
return DataTypes.TINYINT;
} else if (CqlDuration.class.equals(javaClass)) {
return DataTypes.DURATION;
} else if (UdtValue.class.isAssignableFrom(javaClass)) {
return ((UdtValue) value).getType();
} else if (TupleValue.class.isAssignableFrom(javaClass)) {
return ((TupleValue) value).getType();
}
// This might mean that the java type is a custom type with a custom codec,
// so don't throw CodecNotFoundException just yet.
return null;
}
private TypeCodec