blob: 7416d491433860913b30ca6e3dcc590e1d3ef26b [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.db.marshal;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.*;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.cql3.FieldIdentifier;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
/**
* Parse a string containing an Type definition.
*/
public class TypeParser
{
private final String str;
private int idx;
// A cache of parsed string, specially useful for DynamicCompositeType
private static volatile ImmutableMap<String, AbstractType<?>> cache = ImmutableMap.of();
public static final TypeParser EMPTY_PARSER = new TypeParser("", 0);
private TypeParser(String str, int idx)
{
this.str = str;
this.idx = idx;
}
public TypeParser(String str)
{
this(str, 0);
}
/**
* Parse a string containing an type definition.
*/
public static AbstractType<?> parse(String str) throws SyntaxException, ConfigurationException
{
if (str == null)
return BytesType.instance;
// A single volatile read of 'cache' should not hurt.
AbstractType<?> type = cache.get(str);
if (type != null)
return type;
// This could be simplier (i.e. new TypeParser(str).parse()) but we avoid creating a TypeParser object if not really necessary.
int i = 0;
i = skipBlank(str, i);
int j = i;
while (!isEOS(str, i) && isIdentifierChar(str.charAt(i)))
++i;
if (i == j)
return BytesType.instance;
String name = str.substring(j, i);
i = skipBlank(str, i);
if (!isEOS(str, i) && str.charAt(i) == '(')
type = getAbstractType(name, new TypeParser(str, i));
else
type = getAbstractType(name);
Verify.verify(type != null, "Parsing %s yielded null, which is a bug", str);
// Prevent concurrent modification to the map acting as the cache for TypeParser at the expense of
// more allocation when the cache needs to be updated, since updates to the cache are rare compared
// to the amount of reads.
//
// Copy the existing cache into a new map and add the parsed AbstractType instance and replace
// the cache, if the type is not already in the cache.
//
// The cache-update is done in a short synchronized block to prevent duplicate instances of AbstractType
// for the same string representation.
synchronized (TypeParser.class)
{
if (!cache.containsKey(str))
{
ImmutableMap.Builder<String, AbstractType<?>> builder = ImmutableMap.builder();
builder.putAll(cache).put(str, type);
cache = builder.build();
}
return type;
}
}
public static AbstractType<?> parse(CharSequence compareWith) throws SyntaxException, ConfigurationException
{
return parse(compareWith == null ? null : compareWith.toString());
}
/**
* Parse an AbstractType from current position of this parser.
*/
public AbstractType<?> parse() throws SyntaxException, ConfigurationException
{
skipBlank();
String name = readNextIdentifier();
skipBlank();
if (!isEOS() && str.charAt(idx) == '(')
return getAbstractType(name, this);
else
return getAbstractType(name);
}
public Map<String, String> getKeyValueParameters() throws SyntaxException
{
if (isEOS())
return Collections.emptyMap();
if (str.charAt(idx) != '(')
throw new IllegalStateException();
Map<String, String> map = new HashMap<>();
++idx; // skipping '('
while (skipBlankAndComma())
{
if (str.charAt(idx) == ')')
{
++idx;
return map;
}
String k = readNextIdentifier();
String v = "";
skipBlank();
if (str.charAt(idx) == '=')
{
++idx;
skipBlank();
v = readNextIdentifier();
}
else if (str.charAt(idx) != ',' && str.charAt(idx) != ')')
{
throwSyntaxError("unexpected character '" + str.charAt(idx) + "'");
}
map.put(k, v);
}
throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
public List<AbstractType<?>> getTypeParameters() throws SyntaxException, ConfigurationException
{
List<AbstractType<?>> 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(parse());
}
catch (SyntaxException e)
{
SyntaxException ex = new SyntaxException(String.format("Exception while parsing '%s' around char %d", str, idx));
ex.initCause(e);
throw ex;
}
}
throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
public Map<Byte, AbstractType<?>> getAliasParameters() throws SyntaxException, ConfigurationException
{
Map<Byte, AbstractType<?>> map = new HashMap<>();
if (isEOS())
return map;
if (str.charAt(idx) != '(')
throw new IllegalStateException();
++idx; // skipping '('
while (skipBlankAndComma())
{
if (str.charAt(idx) == ')')
{
++idx;
return map;
}
String alias = readNextIdentifier();
if (alias.length() != 1)
throwSyntaxError("An alias should be a single character");
char aliasChar = alias.charAt(0);
if (aliasChar < 33 || aliasChar > 127)
throwSyntaxError("An alias should be a single character in [0..9a..bA..B-+._&]");
skipBlank();
if (!(str.charAt(idx) == '=' && str.charAt(idx+1) == '>'))
throwSyntaxError("expecting '=>' token");
idx += 2;
skipBlank();
try
{
map.put((byte)aliasChar, parse());
}
catch (SyntaxException e)
{
SyntaxException ex = new SyntaxException(String.format("Exception while parsing '%s' around char %d", str, idx));
ex.initCause(e);
throw ex;
}
}
throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
public Map<ByteBuffer, CollectionType> getCollectionsParameters() throws SyntaxException, ConfigurationException
{
Map<ByteBuffer, CollectionType> map = new HashMap<>();
if (isEOS())
return map;
if (str.charAt(idx) != '(')
throw new IllegalStateException();
++idx; // skipping '('
while (skipBlankAndComma())
{
if (str.charAt(idx) == ')')
{
++idx;
return map;
}
ByteBuffer bb = fromHex(readNextIdentifier());
skipBlank();
if (str.charAt(idx) != ':')
throwSyntaxError("expecting ':' token");
++idx;
skipBlank();
try
{
AbstractType<?> type = parse();
if (!(type instanceof CollectionType))
throw new SyntaxException(type + " is not a collection type");
map.put(bb, (CollectionType)type);
}
catch (SyntaxException e)
{
SyntaxException ex = new SyntaxException(String.format("Exception while parsing '%s' around char %d", str, idx));
ex.initCause(e);
throw ex;
}
}
throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
private ByteBuffer fromHex(String hex) throws SyntaxException
{
try
{
return ByteBufferUtil.hexToBytes(hex);
}
catch (NumberFormatException e)
{
throwSyntaxError(e.getMessage());
return null;
}
}
public Pair<Pair<String, ByteBuffer>, List<Pair<ByteBuffer, AbstractType>>> getUserTypeParameters() throws SyntaxException, ConfigurationException
{
if (isEOS() || str.charAt(idx) != '(')
throw new IllegalStateException();
++idx; // skipping '('
skipBlankAndComma();
String keyspace = readNextIdentifier();
skipBlankAndComma();
ByteBuffer typeName = fromHex(readNextIdentifier());
List<Pair<ByteBuffer, AbstractType>> defs = new ArrayList<>();
while (skipBlankAndComma())
{
if (str.charAt(idx) == ')')
{
++idx;
return Pair.create(Pair.create(keyspace, typeName), defs);
}
ByteBuffer name = fromHex(readNextIdentifier());
skipBlank();
if (str.charAt(idx) != ':')
throwSyntaxError("expecting ':' token");
++idx;
skipBlank();
try
{
AbstractType type = parse();
defs.add(Pair.create(name, type));
}
catch (SyntaxException e)
{
SyntaxException ex = new SyntaxException(String.format("Exception while parsing '%s' around char %d", str, idx));
ex.initCause(e);
throw ex;
}
}
throw new SyntaxException(String.format("Syntax error parsing '%s' at char %d: unexpected end of string", str, idx));
}
private static AbstractType<?> getAbstractType(String compareWith) throws ConfigurationException
{
String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith;
Class<? extends AbstractType<?>> typeClass = FBUtilities.<AbstractType<?>>classForName(className, "abstract-type");
try
{
Field field = typeClass.getDeclaredField("instance");
return (AbstractType<?>) field.get(null);
}
catch (NoSuchFieldException | IllegalAccessException e)
{
// Trying with empty parser
return getRawAbstractType(typeClass, EMPTY_PARSER);
}
}
private static AbstractType<?> getAbstractType(String compareWith, TypeParser parser) throws SyntaxException, ConfigurationException
{
String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith;
Class<? extends AbstractType<?>> typeClass = FBUtilities.<AbstractType<?>>classForName(className, "abstract-type");
try
{
Method method = typeClass.getDeclaredMethod("getInstance", TypeParser.class);
return (AbstractType<?>) method.invoke(null, parser);
}
catch (NoSuchMethodException | IllegalAccessException e)
{
// Trying to see if we have an instance field and apply the default parameter to it
AbstractType<?> type = getRawAbstractType(typeClass);
return AbstractType.parseDefaultParameters(type, parser);
}
catch (InvocationTargetException e)
{
ConfigurationException ex = new ConfigurationException("Invalid definition for comparator " + typeClass.getName() + ".");
ex.initCause(e.getTargetException());
throw ex;
}
}
private static AbstractType<?> getRawAbstractType(Class<? extends AbstractType<?>> typeClass) throws ConfigurationException
{
try
{
Field field = typeClass.getDeclaredField("instance");
return (AbstractType<?>) field.get(null);
}
catch (NoSuchFieldException | IllegalAccessException e)
{
throw new ConfigurationException("Invalid comparator class " + typeClass.getName() + ": must define a public static instance field or a public static method getInstance(TypeParser).");
}
}
private static AbstractType<?> getRawAbstractType(Class<? extends AbstractType<?>> typeClass, TypeParser parser) throws ConfigurationException
{
try
{
Method method = typeClass.getDeclaredMethod("getInstance", TypeParser.class);
return (AbstractType<?>) method.invoke(null, parser);
}
catch (NoSuchMethodException | IllegalAccessException e)
{
throw new ConfigurationException("Invalid comparator class " + typeClass.getName() + ": must define a public static instance field or a public static method getInstance(TypeParser).");
}
catch (InvocationTargetException e)
{
ConfigurationException ex = new ConfigurationException("Invalid definition for comparator " + typeClass.getName() + ".");
ex.initCause(e.getTargetException());
throw ex;
}
}
private void throwSyntaxError(String msg) throws SyntaxException
{
throw new SyntaxException(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 static boolean isBlank(int c)
{
return c == ' ' || c == '\t' || c == '\n';
}
private void skipBlank()
{
idx = skipBlank(str, idx);
}
private static int skipBlank(String str, int i)
{
while (!isEOS(str, i) && 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 (!isBlank(c))
{
return true;
}
++idx;
}
return false;
}
/*
* [0..9a..bA..B-+._&]
*/
private static boolean isIdentifierChar(int c)
{
return (c >= '0' && c <= '9')
|| (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| c == '-' || c == '+' || c == '.' || c == '_' || c == '&';
}
// left idx positioned on the character stopping the read
public String readNextIdentifier()
{
int i = idx;
while (!isEOS() && isIdentifierChar(str.charAt(idx)))
++idx;
return str.substring(i, idx);
}
/**
* Helper function to ease the writing of AbstractType.toString() methods.
*/
public static String stringifyAliasesParameters(Map<Byte, AbstractType<?>> aliases)
{
StringBuilder sb = new StringBuilder();
sb.append('(');
Iterator<Map.Entry<Byte, AbstractType<?>>> iter = aliases.entrySet().iterator();
if (iter.hasNext())
{
Map.Entry<Byte, AbstractType<?>> entry = iter.next();
sb.append((char)(byte)entry.getKey()).append("=>").append(entry.getValue());
}
while (iter.hasNext())
{
Map.Entry<Byte, AbstractType<?>> entry = iter.next();
sb.append(',').append((char)(byte)entry.getKey()).append("=>").append(entry.getValue());
}
sb.append(')');
return sb.toString();
}
/**
* Helper function to ease the writing of AbstractType.toString() methods.
*/
public static String stringifyTypeParameters(List<AbstractType<?>> types)
{
return stringifyTypeParameters(types, false);
}
/**
* Helper function to ease the writing of AbstractType.toString() methods.
*/
public static String stringifyTypeParameters(List<AbstractType<?>> types, boolean ignoreFreezing)
{
StringBuilder sb = new StringBuilder("(");
for (int i = 0; i < types.size(); i++)
{
if (i > 0)
sb.append(",");
sb.append(types.get(i).toString(ignoreFreezing));
}
return sb.append(')').toString();
}
public static String stringifyCollectionsParameters(Map<ByteBuffer, ? extends CollectionType> collections)
{
StringBuilder sb = new StringBuilder();
sb.append('(');
boolean first = true;
for (Map.Entry<ByteBuffer, ? extends CollectionType> entry : collections.entrySet())
{
if (!first)
sb.append(',');
first = false;
sb.append(ByteBufferUtil.bytesToHex(entry.getKey())).append(":");
sb.append(entry.getValue());
}
sb.append(')');
return sb.toString();
}
public static String stringifyUserTypeParameters(String keysace, ByteBuffer typeName, List<FieldIdentifier> fields,
List<AbstractType<?>> columnTypes, boolean ignoreFreezing)
{
StringBuilder sb = new StringBuilder();
sb.append('(').append(keysace).append(",").append(ByteBufferUtil.bytesToHex(typeName));
for (int i = 0; i < fields.size(); i++)
{
sb.append(',');
sb.append(ByteBufferUtil.bytesToHex(fields.get(i).bytes)).append(":");
sb.append(columnTypes.get(i).toString(ignoreFreezing));
}
sb.append(')');
return sb.toString();
}
}