| /* |
| * 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); |
| } |
| } |
| } |