blob: 7064ba22c7badfc79dc6c503dd914c83a7fa3901 [file] [log] [blame]
/*
* 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 org.apache.cassandra.cql3.functions.types;
import java.util.*;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.cql3.functions.types.exceptions.DriverInternalError;
import org.apache.cassandra.cql3.functions.types.utils.Bytes;
/*
* Parse data types from schema tables, for Cassandra 3.0 and above.
* 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 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.
*
* Note that those methods all throw DriverInternalError when there is a parsing
* problem because in theory we'll only parse class names coming from Cassandra and
* so there shouldn't be anything wrong with them.
*/
public class DataTypeClassNameParser
{
private static final Logger logger = LoggerFactory.getLogger(DataTypeClassNameParser.class);
private static final String REVERSED_TYPE = "org.apache.cassandra.db.marshal.ReversedType";
private static final String FROZEN_TYPE = "org.apache.cassandra.db.marshal.FrozenType";
private static final String LIST_TYPE = "org.apache.cassandra.db.marshal.ListType";
private static final String SET_TYPE = "org.apache.cassandra.db.marshal.SetType";
private static final String MAP_TYPE = "org.apache.cassandra.db.marshal.MapType";
private static final String UDT_TYPE = "org.apache.cassandra.db.marshal.UserType";
private static final String TUPLE_TYPE = "org.apache.cassandra.db.marshal.TupleType";
private static final String DURATION_TYPE = "org.apache.cassandra.db.marshal.DurationType";
private static final ImmutableMap<String, DataType> cassTypeToDataType =
new ImmutableMap.Builder<String, DataType>()
.put("org.apache.cassandra.db.marshal.AsciiType", DataType.ascii())
.put("org.apache.cassandra.db.marshal.LongType", DataType.bigint())
.put("org.apache.cassandra.db.marshal.BytesType", DataType.blob())
.put("org.apache.cassandra.db.marshal.BooleanType", DataType.cboolean())
.put("org.apache.cassandra.db.marshal.CounterColumnType", DataType.counter())
.put("org.apache.cassandra.db.marshal.DecimalType", DataType.decimal())
.put("org.apache.cassandra.db.marshal.DoubleType", DataType.cdouble())
.put("org.apache.cassandra.db.marshal.FloatType", DataType.cfloat())
.put("org.apache.cassandra.db.marshal.InetAddressType", DataType.inet())
.put("org.apache.cassandra.db.marshal.Int32Type", DataType.cint())
.put("org.apache.cassandra.db.marshal.UTF8Type", DataType.text())
.put("org.apache.cassandra.db.marshal.TimestampType", DataType.timestamp())
.put("org.apache.cassandra.db.marshal.SimpleDateType", DataType.date())
.put("org.apache.cassandra.db.marshal.TimeType", DataType.time())
.put("org.apache.cassandra.db.marshal.UUIDType", DataType.uuid())
.put("org.apache.cassandra.db.marshal.IntegerType", DataType.varint())
.put("org.apache.cassandra.db.marshal.TimeUUIDType", DataType.timeuuid())
.put("org.apache.cassandra.db.marshal.ByteType", DataType.tinyint())
.put("org.apache.cassandra.db.marshal.ShortType", DataType.smallint())
.put(DURATION_TYPE, DataType.duration())
.build();
public static DataType parseOne(
String className, ProtocolVersion protocolVersion, CodecRegistry codecRegistry)
{
boolean frozen = false;
if (isReversed(className))
{
// Just skip the ReversedType part, we don't care
className = getNestedClassName(className);
}
else if (isFrozen(className))
{
frozen = true;
className = getNestedClassName(className);
}
Parser parser = new Parser(className, 0);
String next = parser.parseNextName();
if (next.startsWith(LIST_TYPE))
return DataType.list(
parseOne(parser.getTypeParameters().get(0), protocolVersion, codecRegistry), frozen);
if (next.startsWith(SET_TYPE))
return DataType.set(
parseOne(parser.getTypeParameters().get(0), protocolVersion, codecRegistry), frozen);
if (next.startsWith(MAP_TYPE))
{
List<String> params = parser.getTypeParameters();
return DataType.map(
parseOne(params.get(0), protocolVersion, codecRegistry),
parseOne(params.get(1), protocolVersion, codecRegistry),
frozen);
}
if (frozen)
logger.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");
if (isUserType(next))
{
++parser.idx; // skipping '('
String keyspace = parser.readOne();
parser.skipBlankAndComma();
String typeName =
TypeCodec.varchar()
.deserialize(Bytes.fromHexString("0x" + parser.readOne()), protocolVersion);
parser.skipBlankAndComma();
Map<String, String> rawFields = parser.getNameAndTypeParameters();
List<UserType.Field> fields = new ArrayList<>(rawFields.size());
for (Map.Entry<String, String> entry : rawFields.entrySet())
fields.add(
new UserType.Field(
entry.getKey(), parseOne(entry.getValue(), protocolVersion, codecRegistry)));
// create a frozen UserType since C* 2.x UDTs are always frozen.
return new UserType(keyspace, typeName, true, fields, protocolVersion, codecRegistry);
}
if (isTupleType(next))
{
List<String> rawTypes = parser.getTypeParameters();
List<DataType> types = new ArrayList<>(rawTypes.size());
for (String rawType : rawTypes)
{
types.add(parseOne(rawType, protocolVersion, codecRegistry));
}
return new TupleType(types, protocolVersion, codecRegistry);
}
DataType type = cassTypeToDataType.get(next);
return type == null ? DataType.custom(className) : type;
}
public static boolean isReversed(String className)
{
return className.startsWith(REVERSED_TYPE);
}
public static boolean isFrozen(String className)
{
return className.startsWith(FROZEN_TYPE);
}
private static String getNestedClassName(String className)
{
Parser p = new Parser(className, 0);
p.parseNextName();
List<String> l = p.getTypeParameters();
if (l.size() != 1) throw new IllegalStateException();
className = l.get(0);
return className;
}
private static boolean isUserType(String className)
{
return className.startsWith(UDT_TYPE);
}
private static boolean isTupleType(String className)
{
return className.startsWith(TUPLE_TYPE);
}
private static class Parser
{
private final String str;
private int idx;
private Parser(String str, int idx)
{
this.str = str;
this.idx = idx;
}
String parseNextName()
{
skipBlank();
return readNextIdentifier();
}
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<String> getTypeParameters()
{
List<String> 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;
}
try
{
list.add(readOne());
}
catch (DriverInternalError e)
{
throw new DriverInternalError(
String.format("Exception while parsing '%s' around char %d", str, idx), e);
}
}
throw new DriverInternalError(
String.format(
"Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
// Must be at the start of the first parameter to read
Map<String, String> getNameAndTypeParameters()
{
// The order of the hashmap matters for UDT
Map<String, String> map = new LinkedHashMap<>();
while (skipBlankAndComma())
{
if (str.charAt(idx) == ')')
{
++idx;
return map;
}
String bbHex = readNextIdentifier();
String name = null;
try
{
name =
TypeCodec.varchar()
.deserialize(Bytes.fromHexString("0x" + bbHex), ProtocolVersion.CURRENT);
}
catch (NumberFormatException e)
{
throwSyntaxError(e.getMessage());
}
skipBlank();
if (str.charAt(idx) != ':') throwSyntaxError("expecting ':' token");
++idx;
skipBlank();
try
{
map.put(name, readOne());
}
catch (DriverInternalError e)
{
throw new DriverInternalError(
String.format("Exception while parsing '%s' around char %d", str, idx), e);
}
}
throw new DriverInternalError(
String.format(
"Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
private void throwSyntaxError(String msg)
{
throw new DriverInternalError(
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
String readNextIdentifier()
{
int i = idx;
while (!isEOS() && ParseUtils.isIdentifierChar(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);
}
}
}