/* * 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; import com.datastax.driver.core.exceptions.DriverInternalError; import com.datastax.driver.core.exceptions.UnresolvedUserTypeException; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.List; import java.util.Map; import static com.datastax.driver.core.DataType.*; import static com.datastax.driver.core.ParseUtils.*; /* * Parse data types from schema tables, for Cassandra 3.0 and above. * In these versions, data types appear as string literals, like "ascii" or "tuple<int,int>". * * Note that these 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. */ class DataTypeCqlNameParser { private static final String FROZEN = "frozen"; private static final String LIST = "list"; private static final String SET = "set"; private static final String MAP = "map"; private static final String TUPLE = "tuple"; private static final String EMPTY = "empty"; private static final ImmutableMap<String, DataType> NATIVE_TYPES_MAP = new ImmutableMap.Builder<String, DataType>() .put("ascii", ascii()) .put("bigint", bigint()) .put("blob", blob()) .put("boolean", cboolean()) .put("counter", counter()) .put("decimal", decimal()) .put("double", cdouble()) .put("float", cfloat()) .put("inet", inet()) .put("int", cint()) .put("text", text()) .put("varchar", varchar()) .put("timestamp", timestamp()) .put("date", date()) .put("time", time()) .put("uuid", uuid()) .put("varint", varint()) .put("timeuuid", timeuuid()) .put("tinyint", tinyint()) .put("smallint", smallint()) // duration is not really a native CQL type, but appears as so in system tables .put("duration", duration()) .build(); /** * @param currentUserTypes if this method gets called as part of a refresh that spans multiple user types, this contains the ones * that have already been refreshed. If the type we are parsing references a user type, we want to pick its * definition from this map in priority. * @param oldUserTypes this contains all the keyspace's user types as they were before the refresh started. If we can't find a * definition in {@code currentUserTypes}, we'll check this map as a fallback. */ static DataType parse(String toParse, Cluster cluster, String currentKeyspaceName, Map<String, UserType> currentUserTypes, Map<String, UserType> oldUserTypes, boolean frozen, boolean shallowUserTypes) { if (toParse.startsWith("'")) return custom(toParse.substring(1, toParse.length() - 1)); Parser parser = new Parser(toParse, 0); String type = parser.parseTypeName(); DataType nativeType = NATIVE_TYPES_MAP.get(type.toLowerCase()); if (nativeType != null) return nativeType; if (type.equalsIgnoreCase(LIST)) { List<String> parameters = parser.parseTypeParameters(); if (parameters.size() != 1) throw new DriverInternalError(String.format("Excepting single parameter for list, got %s", parameters)); DataType elementType = parse(parameters.get(0), cluster, currentKeyspaceName, currentUserTypes, oldUserTypes, false, shallowUserTypes); return list(elementType, frozen); } if (type.equalsIgnoreCase(SET)) { List<String> parameters = parser.parseTypeParameters(); if (parameters.size() != 1) throw new DriverInternalError(String.format("Excepting single parameter for set, got %s", parameters)); DataType elementType = parse(parameters.get(0), cluster, currentKeyspaceName, currentUserTypes, oldUserTypes, false, shallowUserTypes); return set(elementType, frozen); } if (type.equalsIgnoreCase(MAP)) { List<String> parameters = parser.parseTypeParameters(); if (parameters.size() != 2) throw new DriverInternalError(String.format("Excepting two parameters for map, got %s", parameters)); DataType keyType = parse(parameters.get(0), cluster, currentKeyspaceName, currentUserTypes, oldUserTypes, false, shallowUserTypes); DataType valueType = parse(parameters.get(1), cluster, currentKeyspaceName, currentUserTypes, oldUserTypes, false, shallowUserTypes); return map(keyType, valueType, frozen); } if (type.equalsIgnoreCase(FROZEN)) { List<String> parameters = parser.parseTypeParameters(); if (parameters.size() != 1) throw new DriverInternalError(String.format("Excepting single parameter for frozen keyword, got %s", parameters)); return parse(parameters.get(0), cluster, currentKeyspaceName, currentUserTypes, oldUserTypes, true, shallowUserTypes); } if (type.equalsIgnoreCase(TUPLE)) { List<String> rawTypes = parser.parseTypeParameters(); List<DataType> types = new ArrayList<DataType>(rawTypes.size()); for (String rawType : rawTypes) { types.add(parse(rawType, cluster, currentKeyspaceName, currentUserTypes, oldUserTypes, false, shallowUserTypes)); } return cluster.getMetadata().newTupleType(types); } // return a custom type for the special empty type // so that it gets detected later on, see TableMetadata if (type.equalsIgnoreCase(EMPTY)) return custom(type); // We need to remove escaped double quotes within the type name as it is stored unescaped. // Otherwise it's a UDT. If we only want a shallow definition build it, otherwise search known definitions. if (shallowUserTypes) return new UserType.Shallow(currentKeyspaceName, Metadata.handleId(type), frozen); UserType userType = null; if (currentUserTypes != null) userType = currentUserTypes.get(Metadata.handleId(type)); if (userType == null && oldUserTypes != null) userType = oldUserTypes.get(Metadata.handleId(type)); if (userType == null) throw new UnresolvedUserTypeException(currentKeyspaceName, type); else return userType.copy(frozen); } private static class Parser { private final String str; private int idx; Parser(String str, int idx) { this.str = str; this.idx = idx; } String parseTypeName() { idx = skipSpaces(str, idx); return readNextIdentifier(); } List<String> parseTypeParameters() { List<String> list = new ArrayList<String>(); if (isEOS()) return list; skipBlankAndComma(); if (str.charAt(idx) != '<') throw new IllegalStateException(); ++idx; // skipping '<' while (skipBlankAndComma()) { if (str.charAt(idx) == '>') { ++idx; return list; } try { String name = parseTypeName(); String args = readRawTypeParameters(); list.add(name + args); } catch (DriverInternalError e) { DriverInternalError ex = new DriverInternalError(String.format("Exception while parsing '%s' around char %d", str, idx)); ex.initCause(e); throw ex; } } throw new DriverInternalError(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); } // left idx positioned on the character stopping the read private String readNextIdentifier() { int startIdx = idx; if (str.charAt(startIdx) == '"') { // case-sensitive name included in double quotes ++idx; // read until closing quote. while (!isEOS()) { boolean atQuote = str.charAt(idx) == '"'; ++idx; if (atQuote) { // if the next character is also a quote, this is an escaped // quote, continue reading, otherwise stop. if (!isEOS() && str.charAt(idx) == '"') ++idx; else break; } } } else if (str.charAt(startIdx) == '\'') { // custom type name included in single quotes ++idx; // read until closing quote. while (!isEOS() && str.charAt(idx++) != '\'') { /* loop */ } } else { while (!isEOS() && (isIdentifierChar(str.charAt(idx)) || str.charAt(idx) == '"')) ++idx; } return str.substring(startIdx, idx); } // Assumes we have just read a type 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 brackets. private String readRawTypeParameters() { idx = skipSpaces(str, idx); 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; boolean inQuotes = false; while (open > 0) { ++idx; if (isEOS()) throw new IllegalStateException("Non closed angle brackets"); // Only parse for '<' and '>' characters if not within a quoted identifier. // Note we don't need to handle escaped quotes ("") in type names here, because they just cause inQuotes to flip // to false and immediately back to true if (!inQuotes) { if (str.charAt(idx) == '"') { inQuotes = true; } else if (str.charAt(idx) == '<') { open++; } else if (str.charAt(idx) == '>') { open--; } } else if (str.charAt(idx) == '"') { inQuotes = false; } } // we've stopped at the last closing ')' so move past that ++idx; return str.substring(i, idx); } // 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 (!isBlank(c)) { return true; } ++idx; } return false; } private boolean isEOS() { return idx >= str.length(); } @Override public String toString() { return str.substring(0, idx) + "[" + (idx == str.length() ? "" : str.charAt(idx)) + "]" + str.substring(idx + 1); } } }