package org.apache.cassandra.thrift;
/*
*
* 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.
*
*/
import java.util.Arrays;
import org.apache.commons.lang.ArrayUtils;
import org.apache.cassandra.db.IncrementCounterClock;
import org.apache.cassandra.db.KeyspaceNotDefinedException;
import org.apache.cassandra.db.ColumnFamily;
import org.apache.cassandra.db.IColumn;
import org.apache.cassandra.db.ColumnFamilyType;
import org.apache.cassandra.db.IClock;
import org.apache.cassandra.db.TimestampClock;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.MarshalException;
import java.util.Comparator;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.MarshalException;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.RandomPartitioner;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
public class ThriftValidation
{
private static final Logger logger = LoggerFactory.getLogger(ThriftValidation.class);
static void validateKey(byte[] key) throws InvalidRequestException
{
if (key == null || key.length == 0)
{
throw new InvalidRequestException("Key may not be empty");
}
// check that key can be handled by FBUtilities.writeShortByteArray
if (key.length > FBUtilities.MAX_UNSIGNED_SHORT)
{
throw new InvalidRequestException("Key length of " + key.length +
" is longer than maximum of " + FBUtilities.MAX_UNSIGNED_SHORT);
}
}
private static void validateTable(String tablename) throws KeyspaceNotDefinedException
{
if (!DatabaseDescriptor.getTables().contains(tablename))
{
throw new KeyspaceNotDefinedException("Keyspace " + tablename + " does not exist in this schema.");
}
}
public static ColumnFamilyType validateColumnFamily(String tablename, String cfName) throws InvalidRequestException
{
if (cfName.isEmpty())
{
throw new InvalidRequestException("non-empty columnfamily is required");
}
ColumnFamilyType cfType = DatabaseDescriptor.getColumnFamilyType(tablename, cfName);
if (cfType == null)
{
throw new InvalidRequestException("unconfigured columnfamily " + cfName);
}
return cfType;
}
static void validateColumnPath(String tablename, ColumnPath column_path) throws InvalidRequestException
{
validateTable(tablename);
ColumnFamilyType cfType = validateColumnFamily(tablename, column_path.column_family);
if (cfType == ColumnFamilyType.Standard)
{
if (column_path.super_column != null)
{
throw new InvalidRequestException("supercolumn parameter is invalid for standard CF " + column_path.column_family);
}
if (column_path.column == null)
{
throw new InvalidRequestException("column parameter is not optional for standard CF " + column_path.column_family);
}
}
else
{
if (column_path.super_column == null)
throw new InvalidRequestException("supercolumn parameter is not optional for super CF " + column_path.column_family);
}
if (column_path.column != null)
{
validateColumns(tablename, column_path.column_family, column_path.super_column, Arrays.asList(column_path.column));
}
if (column_path.super_column != null)
{
validateColumns(tablename, column_path.column_family, null, Arrays.asList(column_path.super_column));
}
}
static void validateColumnParent(String tablename, ColumnParent column_parent) throws InvalidRequestException
{
validateTable(tablename);
ColumnFamilyType cfType = validateColumnFamily(tablename, column_parent.column_family);
if (cfType == ColumnFamilyType.Standard)
{
if (column_parent.super_column != null)
{
throw new InvalidRequestException("columnfamily alone is required for standard CF " + column_parent.column_family);
}
}
if (column_parent.super_column != null)
{
validateColumns(tablename, column_parent.column_family, null, Arrays.asList(column_parent.super_column));
}
}
// column_path_or_parent is a ColumnPath for remove, where the "column" is optional even for a standard CF
static void validateColumnPathOrParent(String tablename, ColumnPath column_path_or_parent) throws InvalidRequestException
{
validateTable(tablename);
ColumnFamilyType cfType = validateColumnFamily(tablename, column_path_or_parent.column_family);
if (cfType == ColumnFamilyType.Standard)
{
if (column_path_or_parent.super_column != null)
{
throw new InvalidRequestException("supercolumn may not be specified for standard CF " + column_path_or_parent.column_family);
}
}
if (column_path_or_parent.column != null)
{
validateColumns(tablename, column_path_or_parent.column_family, column_path_or_parent.super_column, Arrays.asList(column_path_or_parent.column));
}
if (column_path_or_parent.super_column != null)
{
validateColumns(tablename, column_path_or_parent.column_family, null, Arrays.asList(column_path_or_parent.super_column));
}
}
private static void validateColumns(String keyspace, String columnFamilyName, byte[] superColumnName, Iterable<byte[]> column_names)
throws InvalidRequestException
{
if (superColumnName != null)
{
if (superColumnName.length > IColumn.MAX_NAME_LENGTH)
throw new InvalidRequestException("supercolumn name length must not be greater than " + IColumn.MAX_NAME_LENGTH);
if (superColumnName.length == 0)
throw new InvalidRequestException("supercolumn name must not be empty");
if (DatabaseDescriptor.getColumnFamilyType(keyspace, columnFamilyName) == ColumnFamilyType.Standard)
throw new InvalidRequestException("supercolumn specified to ColumnFamily " + columnFamilyName + " containing normal columns");
}
AbstractType comparator = ColumnFamily.getComparatorFor(keyspace, columnFamilyName, superColumnName);
for (byte[] name : column_names)
{
if (name.length > IColumn.MAX_NAME_LENGTH)
throw new InvalidRequestException("column name length must not be greater than " + IColumn.MAX_NAME_LENGTH);
if (name.length == 0)
throw new InvalidRequestException("column name must not be empty");
try
{
comparator.validate(name);
}
catch (MarshalException e)
{
throw new InvalidRequestException(e.getMessage());
}
}
}
public static void validateColumns(String keyspace, ColumnParent column_parent, Iterable<byte[]> column_names) throws InvalidRequestException
{
validateColumns(keyspace, column_parent.column_family, column_parent.super_column, column_names);
}
public static void validateRange(String keyspace, ColumnParent column_parent, SliceRange range) throws InvalidRequestException
{
AbstractType comparator = ColumnFamily.getComparatorFor(keyspace, column_parent.column_family, column_parent.super_column);
try
{
comparator.validate(range.start);
comparator.validate(range.finish);
}
catch (MarshalException e)
{
throw new InvalidRequestException(e.getMessage());
}
if (range.count < 0)
throw new InvalidRequestException("get_slice requires non-negative count");
Comparator<byte[]> orderedComparator = range.isReversed() ? comparator.getReverseComparator() : comparator;
if (range.start.length > 0
&& range.finish.length > 0
&& orderedComparator.compare(range.start, range.finish) > 0)
{
throw new InvalidRequestException("range finish must come after start in the order of traversal");
}
}
public static void validateColumnOrSuperColumn(String keyspace, String cfName, ColumnOrSuperColumn cosc)
throws InvalidRequestException
{
if (cosc.column != null)
{
validateTtl(cosc.column);
IClock clock = validateClock(keyspace, cfName, cosc.column.clock);
validateValueByClock(cosc.column.value, clock);
ThriftValidation.validateColumnPath(keyspace, new ColumnPath(cfName).setSuper_column(null).setColumn(cosc.column.name));
}
if (cosc.super_column != null)
{
for (Column c : cosc.super_column.columns)
{
validateTtl(c);
IClock clock = validateClock(keyspace, cfName, c.clock);
validateValueByClock(c.value, clock);
ThriftValidation.validateColumnPath(keyspace, new ColumnPath(cfName).setSuper_column(cosc.super_column.name).setColumn(c.name));
}
}
if (cosc.column == null && cosc.super_column == null)
throw new InvalidRequestException("ColumnOrSuperColumn must have one or both of Column or SuperColumn");
}
private static void validateTtl(Column column) throws InvalidRequestException
{
if (column.isSetTtl() && column.ttl <= 0)
{
throw new InvalidRequestException("ttl must be positive");
}
// if it's not set, then it should be zero -- here we are just checking to make sure Thrift doesn't change that contract with us.
assert column.isSetTtl() || column.ttl == 0;
}
/**
* Fetch the clock type for the provided column family and check that we have all the
* information required to instantiate.
*/
public static IClock validateClock(String keyspace, String cfName, Clock clock) throws InvalidRequestException
{
ClockType clockType = DatabaseDescriptor.getClockType(keyspace, cfName);
if (clockType == null)
throw new InvalidRequestException("No clock found for " + keyspace + " " + cfName);
switch (clockType)
{
case Timestamp:
if (!clock.isSetTimestamp())
{
throw new InvalidRequestException("No timestamp set, despite timestamp clock being used: " + keyspace + " " + cfName);
}
return new TimestampClock(clock.getTimestamp());
case IncrementCounter:
return new IncrementCounterClock();
default:
throw new InvalidRequestException("Invalid clock type for " + keyspace + " " + cfName);
}
}
/**
* Check that the value is valid for the clock specified.
* For example the increment counter cannot accept negative values.
* @param value Value to validate.
* @param cassandraClock Clock to check by.
* @throws InvalidRequestException If the value is invalid this exception is thrown.
*/
public static void validateValueByClock(byte[] value, IClock cassandraClock) throws InvalidRequestException
{
switch (cassandraClock.type())
{
case IncrementCounter:
try
{
long delta = FBUtilities.byteArrayToLong(value);
if (delta < 0)
{
throw new InvalidRequestException("Value must be positive when using an increment counter");
}
}
catch (IllegalArgumentException e)
{
throw new InvalidRequestException("Value is not a valid long delta: " + e.getMessage());
}
break;
case Timestamp:
default:
return; //nothing to check
}
}
public static void validateMutation(String keyspace, String cfName, Mutation mut)
throws InvalidRequestException
{
ColumnOrSuperColumn cosc = mut.column_or_supercolumn;
Deletion del = mut.deletion;
if (cosc != null && del != null)
throw new InvalidRequestException("Mutation may have either a ColumnOrSuperColumn or a Deletion, but not both");
if (cosc != null)
{
validateColumnOrSuperColumn(keyspace, cfName, cosc);
}
else if (del != null)
{
validateDeletion(keyspace, cfName, del);
}
else
{
throw new InvalidRequestException("Mutation must have one ColumnOrSuperColumn or one Deletion");
}
}
public static void validateDeletion(String keyspace, String cfName, Deletion del) throws InvalidRequestException
{
validateColumnFamily(keyspace, cfName);
if (del.predicate != null)
{
validateSlicePredicate(keyspace, cfName, del.super_column, del.predicate);
if (del.predicate.slice_range != null)
throw new InvalidRequestException("Deletion does not yet support SliceRange predicates.");
}
if (ColumnFamilyType.Standard == DatabaseDescriptor.getColumnFamilyType(keyspace, cfName) && del.super_column != null)
{
String msg = String.format("deletion of super_column is not possible on a standard ColumnFamily (KeySpace=%s ColumnFamily=%s Deletion=%s)", keyspace, cfName, del);
throw new InvalidRequestException(msg);
}
}
public static void validateSlicePredicate(String keyspace, String cfName, byte[] scName, SlicePredicate predicate) throws InvalidRequestException
{
if (predicate.column_names == null && predicate.slice_range == null)
throw new InvalidRequestException("A SlicePredicate must be given a list of Columns, a SliceRange, or both");
if (predicate.slice_range != null)
validateRange(keyspace, new ColumnParent(cfName).setSuper_column(scName), predicate.slice_range);
if (predicate.column_names != null)
validateColumns(keyspace, cfName, scName, predicate.column_names);
}
public static void validateColumn(String keyspace, ColumnParent column_parent, Column column) throws InvalidRequestException
{
validateTtl(column);
validateColumns(keyspace, column_parent, Arrays.asList(column.name));
try
{
AbstractType validator = DatabaseDescriptor.getValueValidator(keyspace, column_parent.column_family, column.name);
if (validator != null)
validator.validate(column.value);
}
catch (MarshalException me)
{
throw new InvalidRequestException(String.format("[%s][%s][%s] = [%s] failed validation (%s)",
keyspace,
column_parent.getColumn_family(),
FBUtilities.bytesToHex(column.name),
FBUtilities.bytesToHex(column.value),
me.getMessage()));
}
}
public static void validatePredicate(String keyspace, ColumnParent column_parent, SlicePredicate predicate)
throws InvalidRequestException
{
if (predicate.column_names == null && predicate.slice_range == null)
throw new InvalidRequestException("predicate column_names and slice_range may not both be null");
if (predicate.column_names != null && predicate.slice_range != null)
throw new InvalidRequestException("predicate column_names and slice_range may not both be present");
if (predicate.getSlice_range() != null)
validateRange(keyspace, column_parent, predicate.slice_range);
else
validateColumns(keyspace, column_parent, predicate.column_names);
}
public static void validateKeyRange(KeyRange range) throws InvalidRequestException
{
if ((range.start_key == null) != (range.end_key == null))
{
throw new InvalidRequestException("start key and end key must either both be non-null, or both be null");
}
if ((range.start_token == null) != (range.end_token == null))
{
throw new InvalidRequestException("start token and end token must either both be non-null, or both be null");
}
if ((range.start_key == null) == (range.start_token == null))
{
throw new InvalidRequestException("exactly one of {start key, end key} or {start token, end token} must be specified");
}
if (range.start_key != null)
{
IPartitioner p = StorageService.getPartitioner();
Token startToken = p.getToken(range.start_key);
Token endToken = p.getToken(range.end_key);
if (startToken.compareTo(endToken) > 0 && !endToken.equals(p.getMinimumToken()))
{
if (p instanceof RandomPartitioner)
throw new InvalidRequestException("start key's md5 sorts after end key's md5. this is not allowed; you probably should not specify end key at all, under RandomPartitioner");
else
throw new InvalidRequestException("start key must sort before (or equal to) finish key in your partitioner!");
}
}
if (range.count <= 0)
{
throw new InvalidRequestException("maxRows must be positive");
}
}
public static void validateIndexClauses(String keyspace, String columnFamily, IndexClause index_clause)
throws InvalidRequestException
{
if (index_clause.expressions.isEmpty())
throw new InvalidRequestException("index clause list may not be empty");
Set<byte[]> indexedColumns = Table.open(keyspace).getColumnFamilyStore(columnFamily).getIndexedColumns();
for (IndexExpression expression : index_clause.expressions)
{
if (expression.op.equals(IndexOperator.EQ) && indexedColumns.contains(expression.column_name))
return;
}
throw new InvalidRequestException("No indexed columns present in index clause with operator EQ");
}
}