com.datastax.oss.driver.internal.core.metadata.schema.parsing.DataTypeClassNameParser Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.metadata.schema.parsing;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.DefaultProtocolVersion;
import com.datastax.oss.driver.api.core.detach.AttachmentPoint;
import com.datastax.oss.driver.api.core.type.DataType;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.api.core.type.UserDefinedType;
import com.datastax.oss.driver.api.core.type.codec.TypeCodecs;
import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
import com.datastax.oss.driver.internal.core.type.DefaultTupleType;
import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder;
import com.datastax.oss.driver.internal.core.type.codec.ParseUtils;
import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import com.datastax.oss.protocol.internal.util.Bytes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.jcip.annotations.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses data types from schema tables, for Cassandra 2.2 and below.
*
* In these versions, data types appear as class names, like
* "org.apache.cassandra.db.marshal.AsciiType" or
* "org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.Int32Type)".
*
*
This is modified (and simplified) from Cassandra's {@code TypeParser} class to suit our needs.
* In particular it's not very efficient, but it doesn't really matter since it's rarely used and
* never in a critical path.
*/
@ThreadSafe
public class DataTypeClassNameParser implements DataTypeParser {
private static final Logger LOG = LoggerFactory.getLogger(DataTypeClassNameParser.class);
@Override
public DataType parse(
CqlIdentifier keyspaceId,
String toParse,
Map userTypes,
InternalDriverContext context) {
// We take keyspaceId as a parameter because of the parent interface, but it's actually unused
// by this implementation.
return parse(toParse, userTypes, context, context.getSessionName());
}
/**
* Simplified parse method for external use.
*
* This is intended for use in Cassandra's UDF implementation (the current version uses the
* similar method from driver 3).
*/
public DataType parse(String toParse, AttachmentPoint attachmentPoint) {
return parse(
toParse,
null, // No caching of user types: nested types will always be fully re-parsed
attachmentPoint,
"parser");
}
private DataType parse(
String toParse,
Map userTypes,
AttachmentPoint attachmentPoint,
String logPrefix) {
boolean frozen = false;
if (isReversed(toParse)) {
// Just skip the ReversedType part, we don't care
toParse = getNestedClassName(toParse);
} else if (toParse.startsWith("org.apache.cassandra.db.marshal.FrozenType")) {
frozen = true;
toParse = getNestedClassName(toParse);
}
Parser parser = new Parser(toParse, 0);
String next = parser.parseNextName();
if (next.startsWith("org.apache.cassandra.db.marshal.ListType")) {
DataType elementType =
parse(parser.getTypeParameters().get(0), userTypes, attachmentPoint, logPrefix);
return DataTypes.listOf(elementType, frozen);
}
if (next.startsWith("org.apache.cassandra.db.marshal.SetType")) {
DataType elementType =
parse(parser.getTypeParameters().get(0), userTypes, attachmentPoint, logPrefix);
return DataTypes.setOf(elementType, frozen);
}
if (next.startsWith("org.apache.cassandra.db.marshal.MapType")) {
List parameters = parser.getTypeParameters();
DataType keyType = parse(parameters.get(0), userTypes, attachmentPoint, logPrefix);
DataType valueType = parse(parameters.get(1), userTypes, attachmentPoint, logPrefix);
return DataTypes.mapOf(keyType, valueType, frozen);
}
if (frozen)
LOG.warn(
"[{}] Got o.a.c.db.marshal.FrozenType for something else than a collection, "
+ "this driver version might be too old for your version of Cassandra",
logPrefix);
if (next.startsWith("org.apache.cassandra.db.marshal.UserType")) {
++parser.idx; // skipping '('
CqlIdentifier keyspace = CqlIdentifier.fromInternal(parser.readOne());
parser.skipBlankAndComma();
String typeName =
TypeCodecs.TEXT.decode(
Bytes.fromHexString("0x" + parser.readOne()), attachmentPoint.getProtocolVersion());
if (typeName == null) {
throw new AssertionError("Type name cannot be null, this is a server bug");
}
CqlIdentifier typeId = CqlIdentifier.fromInternal(typeName);
Map nameAndTypeParameters = parser.getNameAndTypeParameters();
// Avoid re-parsing if we already have the definition
if (userTypes != null && userTypes.containsKey(typeId)) {
// copy as frozen since C* 2.x UDTs are always frozen.
return userTypes.get(typeId).copy(true);
} else {
UserDefinedTypeBuilder builder = new UserDefinedTypeBuilder(keyspace, typeId);
parser.skipBlankAndComma();
for (Map.Entry entry : nameAndTypeParameters.entrySet()) {
CqlIdentifier fieldName = CqlIdentifier.fromInternal(entry.getKey());
DataType fieldType = parse(entry.getValue(), userTypes, attachmentPoint, logPrefix);
builder.withField(fieldName, fieldType);
}
// Create a frozen UserType since C* 2.x UDTs are always frozen.
return builder.frozen().withAttachmentPoint(attachmentPoint).build();
}
}
if (next.startsWith("org.apache.cassandra.db.marshal.TupleType")) {
List rawTypes = parser.getTypeParameters();
ImmutableList.Builder componentTypesBuilder = ImmutableList.builder();
for (String rawType : rawTypes) {
componentTypesBuilder.add(parse(rawType, userTypes, attachmentPoint, logPrefix));
}
return new DefaultTupleType(componentTypesBuilder.build(), attachmentPoint);
}
DataType type = NATIVE_TYPES_BY_CLASS_NAME.get(next);
return type == null ? DataTypes.custom(toParse) : type;
}
static boolean isReversed(String toParse) {
return toParse.startsWith("org.apache.cassandra.db.marshal.ReversedType");
}
private static String getNestedClassName(String className) {
Parser p = new Parser(className, 0);
p.parseNextName();
List l = p.getTypeParameters();
if (l.size() != 1) {
throw new IllegalStateException();
}
className = l.get(0);
return className;
}
static class Parser {
private final String str;
private int idx;
Parser(String str, int idx) {
this.str = str;
this.idx = idx;
}
String parseNextName() {
skipBlank();
return readNextIdentifier();
}
private String readOne() {
String name = parseNextName();
String args = readRawArguments();
return name + args;
}
// Assumes we have just read a class name and read it's potential arguments
// blindly. I.e. it assume that either parsing is done or that we're on a '('
// and this reads everything up until the corresponding closing ')'. It
// returns everything read, including the enclosing parenthesis.
private String readRawArguments() {
skipBlank();
if (isEOS() || str.charAt(idx) == ')' || str.charAt(idx) == ',') {
return "";
}
if (str.charAt(idx) != '(') {
throw new IllegalStateException(
String.format(
"Expecting char %d of %s to be '(' but '%c' found", idx, str, str.charAt(idx)));
}
int i = idx;
int open = 1;
while (open > 0) {
++idx;
if (isEOS()) {
throw new IllegalStateException("Non closed parenthesis");
}
if (str.charAt(idx) == '(') {
open++;
} else if (str.charAt(idx) == ')') {
open--;
}
}
// we've stopped at the last closing ')' so move past that
++idx;
return str.substring(i, idx);
}
List getTypeParameters() {
List list = new ArrayList<>();
if (isEOS()) {
return list;
}
if (str.charAt(idx) != '(') {
throw new IllegalStateException();
}
++idx; // skipping '('
while (skipBlankAndComma()) {
if (str.charAt(idx) == ')') {
++idx;
return list;
}
list.add(readOne());
}
throw new IllegalArgumentException(
String.format(
"Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
Map getCollectionsParameters() {
if (isEOS()) {
return Collections.emptyMap();
}
if (str.charAt(idx) != '(') {
throw new IllegalStateException();
}
++idx; // skipping '('
return getNameAndTypeParameters();
}
// Must be at the start of the first parameter to read
private Map getNameAndTypeParameters() {
// The order of the hashmap matters for UDT
Map map = new LinkedHashMap<>();
while (skipBlankAndComma()) {
if (str.charAt(idx) == ')') {
++idx;
return map;
}
String bbHex = readNextIdentifier();
String name = null;
try {
name =
TypeCodecs.TEXT.decode(
Bytes.fromHexString("0x" + bbHex), DefaultProtocolVersion.DEFAULT);
} catch (NumberFormatException e) {
throwSyntaxError(e.getMessage());
}
skipBlank();
if (str.charAt(idx) != ':') {
throwSyntaxError("expecting ':' token");
}
++idx;
skipBlank();
map.put(name, readOne());
}
throw new IllegalArgumentException(
String.format(
"Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
private void throwSyntaxError(String msg) {
throw new IllegalArgumentException(
String.format("Syntax error parsing '%s' at char %d: %s", str, idx, msg));
}
private boolean isEOS() {
return isEOS(str, idx);
}
private static boolean isEOS(String str, int i) {
return i >= str.length();
}
private void skipBlank() {
idx = skipBlank(str, idx);
}
private static int skipBlank(String str, int i) {
while (!isEOS(str, i) && ParseUtils.isBlank(str.charAt(i))) {
++i;
}
return i;
}
// skip all blank and at best one comma, return true if there not EOS
private boolean skipBlankAndComma() {
boolean commaFound = false;
while (!isEOS()) {
int c = str.charAt(idx);
if (c == ',') {
if (commaFound) {
return true;
} else {
commaFound = true;
}
} else if (!ParseUtils.isBlank(c)) {
return true;
}
++idx;
}
return false;
}
// left idx positioned on the character stopping the read
private String readNextIdentifier() {
int i = idx;
while (!isEOS() && ParseUtils.isCqlIdentifierChar(str.charAt(idx))) {
++idx;
}
return str.substring(i, idx);
}
@Override
public String toString() {
return str.substring(0, idx)
+ "["
+ (idx == str.length() ? "" : str.charAt(idx))
+ "]"
+ str.substring(idx + 1);
}
}
@VisibleForTesting
static ImmutableMap NATIVE_TYPES_BY_CLASS_NAME =
new ImmutableMap.Builder()
.put("org.apache.cassandra.db.marshal.AsciiType", DataTypes.ASCII)
.put("org.apache.cassandra.db.marshal.LongType", DataTypes.BIGINT)
.put("org.apache.cassandra.db.marshal.BytesType", DataTypes.BLOB)
.put("org.apache.cassandra.db.marshal.BooleanType", DataTypes.BOOLEAN)
.put("org.apache.cassandra.db.marshal.CounterColumnType", DataTypes.COUNTER)
.put("org.apache.cassandra.db.marshal.DecimalType", DataTypes.DECIMAL)
.put("org.apache.cassandra.db.marshal.DoubleType", DataTypes.DOUBLE)
.put("org.apache.cassandra.db.marshal.FloatType", DataTypes.FLOAT)
.put("org.apache.cassandra.db.marshal.InetAddressType", DataTypes.INET)
.put("org.apache.cassandra.db.marshal.Int32Type", DataTypes.INT)
.put("org.apache.cassandra.db.marshal.UTF8Type", DataTypes.TEXT)
.put("org.apache.cassandra.db.marshal.TimestampType", DataTypes.TIMESTAMP)
.put("org.apache.cassandra.db.marshal.SimpleDateType", DataTypes.DATE)
.put("org.apache.cassandra.db.marshal.TimeType", DataTypes.TIME)
.put("org.apache.cassandra.db.marshal.UUIDType", DataTypes.UUID)
.put("org.apache.cassandra.db.marshal.IntegerType", DataTypes.VARINT)
.put("org.apache.cassandra.db.marshal.TimeUUIDType", DataTypes.TIMEUUID)
.put("org.apache.cassandra.db.marshal.ByteType", DataTypes.TINYINT)
.put("org.apache.cassandra.db.marshal.ShortType", DataTypes.SMALLINT)
.put("org.apache.cassandra.db.marshal.DurationType", DataTypes.DURATION)
.build();
}