blob: e7a2360fa990ed3554922ec7567c792d52eabb44 [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.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.Term;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.serializers.TypeSerializer;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version;
import org.apache.cassandra.utils.bytecomparable.ByteSource;
import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
import static com.google.common.collect.Iterables.any;
/*
* The encoding of a DynamicCompositeType column name should be:
* <component><component><component> ...
* where <component> is:
* <comparator part><value><'end-of-component' byte>
* where:
* - <comparator part>: either the comparator full name, or a declared
* aliases. This is at least 2 bytes (those 2 bytes are called header in
* the following). If the first bit of the header is 1, then this
* comparator part is an alias, otherwise it's a comparator full name:
* - aliases: the actual alias is the 2nd byte of header taken as a
* character. The whole <comparator part> is thus 2 byte long.
* - comparator full name: the header is the length of the remaining
* part. The remaining part is the UTF-8 encoded comparator class
* name.
* - <value>: the component value bytes preceded by 2 byte containing the
* size of value (see CompositeType).
* - 'end-of-component' byte is defined as in CompositeType
*/
public class DynamicCompositeType extends AbstractCompositeType
{
private static final Logger logger = LoggerFactory.getLogger(DynamicCompositeType.class);
private static final ByteSource[] EMPTY_BYTE_SOURCE_ARRAY = new ByteSource[0];
private static final String REVERSED_TYPE = ReversedType.class.getSimpleName();
private final Map<Byte, AbstractType<?>> aliases;
private final Map<AbstractType<?>, Byte> inverseMapping;
// interning instances
private static final ConcurrentHashMap<Map<Byte, AbstractType<?>>, DynamicCompositeType> instances = new ConcurrentHashMap<>();
public static DynamicCompositeType getInstance(TypeParser parser)
{
return getInstance(parser.getAliasParameters());
}
public static DynamicCompositeType getInstance(Map<Byte, AbstractType<?>> aliases)
{
DynamicCompositeType dct = instances.get(aliases);
return null == dct
? instances.computeIfAbsent(aliases, DynamicCompositeType::new)
: dct;
}
private DynamicCompositeType(Map<Byte, AbstractType<?>> aliases)
{
this.aliases = aliases;
this.inverseMapping = new HashMap<>();
for (Map.Entry<Byte, AbstractType<?>> en : aliases.entrySet())
this.inverseMapping.put(en.getValue(), en.getKey());
}
protected <V> boolean readIsStatic(V value, ValueAccessor<V> accessor)
{
// We don't have the static nothing for DCT
return false;
}
protected int startingOffset(boolean isStatic)
{
return 0;
}
protected <V> int getComparatorSize(int i, V value, ValueAccessor<V> accessor, int offset)
{
int header = accessor.getShort(value, offset);
if ((header & 0x8000) == 0)
{
return 2 + header;
}
else
{
return 2;
}
}
private <V> AbstractType<?> getComparator(V value, ValueAccessor<V> accessor, int offset)
{
try
{
int header = accessor.getShort(value, offset);
if ((header & 0x8000) == 0)
{
String name = accessor.toString(accessor.slice(value, offset + 2, header));
return TypeParser.parse(name);
}
else
{
return aliases.get((byte)(header & 0xFF));
}
}
catch (CharacterCodingException e)
{
throw new RuntimeException(e);
}
}
protected <V> AbstractType<?> getComparator(int i, V value, ValueAccessor<V> accessor, int offset)
{
return getComparator(value, accessor, offset);
}
protected <VL, VR> AbstractType<?> getComparator(int i, VL left, ValueAccessor<VL> accessorL, VR right, ValueAccessor<VR> accessorR, int offsetL, int offsetR)
{
AbstractType<?> comp1 = getComparator(left, accessorL, offsetL);
AbstractType<?> comp2 = getComparator(right, accessorR, offsetR);
AbstractType<?> rawComp = comp1;
/*
* If both types are ReversedType(Type), we need to compare on the wrapped type (which may differ between the two types) to avoid
* incompatible comparisons being made.
*/
if ((comp1 instanceof ReversedType) && (comp2 instanceof ReversedType))
{
comp1 = ((ReversedType<?>) comp1).baseType;
comp2 = ((ReversedType<?>) comp2).baseType;
}
// Fast test if the comparator uses singleton instances
if (comp1 != comp2)
{
/*
* We compare component of different types by comparing the
* comparator class names. We start with the simple classname
* first because that will be faster in almost all cases, but
* fallback on the full name if necessary
*/
int cmp = comp1.getClass().getSimpleName().compareTo(comp2.getClass().getSimpleName());
if (cmp != 0)
return cmp < 0 ? FixedValueComparator.alwaysLesserThan : FixedValueComparator.alwaysGreaterThan;
cmp = comp1.getClass().getName().compareTo(comp2.getClass().getName());
if (cmp != 0)
return cmp < 0 ? FixedValueComparator.alwaysLesserThan : FixedValueComparator.alwaysGreaterThan;
// if cmp == 0, we're actually having the same type, but one that
// did not have a singleton instance. It's ok (though inefficient).
}
// Use the raw comparator (prior to ReversedType unwrapping)
return rawComp;
}
protected <V> AbstractType<?> getAndAppendComparator(int i, V value, ValueAccessor<V> accessor, StringBuilder sb, int offset)
{
try
{
int header = accessor.getShort(value, offset);
if ((header & 0x8000) == 0)
{
String name = accessor.toString(accessor.slice(value, offset + 2, header));
sb.append(name).append("@");
return TypeParser.parse(name);
}
else
{
sb.append((char)(header & 0xFF)).append("@");
return aliases.get((byte)(header & 0xFF));
}
}
catch (CharacterCodingException e)
{
throw new RuntimeException(e);
}
}
@Override
public <V> ByteSource asComparableBytes(ValueAccessor<V> accessor, V data, Version version)
{
List<ByteSource> srcs = new ArrayList<>();
int length = accessor.size(data);
// statics go first
boolean isStatic = readIsStatic(data, accessor);
int offset = startingOffset(isStatic);
srcs.add(isStatic ? null : ByteSource.EMPTY);
byte lastEoc = 0;
int i = 0;
while (offset < length)
{
// Only the end-of-component byte of the last component of this composite can be non-zero, so the
// component before can't have a non-zero end-of-component byte.
assert lastEoc == 0 : lastEoc;
AbstractType<?> comp = getComparator(data, accessor, offset);
offset += getComparatorSize(i, data, accessor, offset);
// The comparable bytes for the component need to ensure comparisons consistent with
// AbstractCompositeType.compareCustom(ByteBuffer, ByteBuffer) and
// DynamicCompositeType.getComparator(int, ByteBuffer, ByteBuffer):
if (version == Version.LEGACY || !(comp instanceof ReversedType))
{
// ...most often that means just adding the short name of the type, followed by the full name of the type.
srcs.add(ByteSource.of(comp.getClass().getSimpleName(), version));
srcs.add(ByteSource.of(comp.getClass().getName(), version));
}
else
{
// ...however some times the component uses a complex type (currently the only supported complex type
// is ReversedType - we can't have elements that are of MapType, CompositeType, TupleType, etc.)...
ReversedType<?> reversedComp = (ReversedType<?>) comp;
// ...in this case, we need to add the short name of ReversedType before the short name of the base
// type, to ensure consistency with DynamicCompositeType.getComparator(int, ByteBuffer, ByteBuffer).
srcs.add(ByteSource.of(REVERSED_TYPE, version));
srcs.add(ByteSource.of(reversedComp.baseType.getClass().getSimpleName(), version));
srcs.add(ByteSource.of(reversedComp.baseType.getClass().getName(), version));
}
// Only then the payload of the component gets encoded.
int componentLength = accessor.getUnsignedShort(data, offset);
offset += 2;
srcs.add(comp.asComparableBytes(accessor, accessor.slice(data, offset, componentLength), version));
offset += componentLength;
// The end-of-component byte also takes part in the comparison, and therefore needs to be encoded.
lastEoc = accessor.getByte(data, offset);
offset += 1;
srcs.add(ByteSource.oneByte(version == Version.LEGACY ? lastEoc : lastEoc & 0xFF ^ 0x80));
++i;
}
return ByteSource.withTerminatorMaybeLegacy(version, ByteSource.END_OF_STREAM, srcs.toArray(EMPTY_BYTE_SOURCE_ARRAY));
}
@Override
public <V> V fromComparableBytes(ValueAccessor<V> accessor, ByteSource.Peekable comparableBytes, Version version)
{
// For ByteComparable.Version.LEGACY the terminator byte is ByteSource.END_OF_STREAM. Just like with
// CompositeType, this means that in multi-component sequences the terminator may be transformed to a regular
// component separator, but unlike CompositeType (where we have the expected number of types/components),
// this can make the end of the whole dynamic composite type indistinguishable from the end of a component
// somewhere in the middle of the dynamic composite type. Because of that, DynamicCompositeType elements
// cannot always be safely decoded using that encoding version.
// Even more so than with CompositeType, we just take advantage of the fact that we don't need to decode from
// Version.LEGACY, assume that we never do that, and assert it here.
assert version != Version.LEGACY;
if (comparableBytes == null)
return accessor.empty();
// The first byte is the isStatic flag which we don't need but must consume to continue past it.
comparableBytes.next();
List<AbstractType<?>> types = new ArrayList<>();
List<V> values = new ArrayList<>();
byte lastEoc = 0;
for (int separator = comparableBytes.next(); separator != ByteSource.TERMINATOR; separator = comparableBytes.next())
{
// Solely the end-of-component byte of the last component of this composite can be non-zero.
assert lastEoc == 0 : lastEoc;
boolean isReversed = false;
// Decode the next type's simple class name that is encoded before its fully qualified class name (in order
// for comparisons to work correctly).
String simpleClassName = ByteSourceInverse.getString(ByteSourceInverse.nextComponentSource(comparableBytes, separator));
if (REVERSED_TYPE.equals(simpleClassName))
{
// Special-handle if the type is reversed (and decode the actual base type simple class name).
isReversed = true;
simpleClassName = ByteSourceInverse.getString(ByteSourceInverse.nextComponentSource(comparableBytes));
}
// Decode the type's fully qualified class name and parse the actual type from it.
String fullClassName = ByteSourceInverse.getString(ByteSourceInverse.nextComponentSource(comparableBytes));
assert fullClassName.endsWith(simpleClassName);
if (isReversed)
fullClassName = REVERSED_TYPE + '(' + fullClassName + ')';
AbstractType<?> type = TypeParser.parse(fullClassName);
assert type != null;
types.add(type);
// Decode the payload from this type.
V value = type.fromComparableBytes(accessor, ByteSourceInverse.nextComponentSource(comparableBytes), version);
values.add(value);
// Also decode the corresponding end-of-component byte - the last one we decode will be taken into
// account when we deserialize the decoded data into an object.
lastEoc = ByteSourceInverse.getSignedByte(ByteSourceInverse.nextComponentSource(comparableBytes));
}
return build(accessor, types, inverseMapping, values, lastEoc);
}
public static ByteBuffer build(List<String> types, List<ByteBuffer> values)
{
return build(ByteBufferAccessor.instance,
Lists.transform(types, TypeParser::parse),
Collections.emptyMap(),
values,
(byte) 0);
}
@VisibleForTesting
public static <V> V build(ValueAccessor<V> accessor,
List<AbstractType<?>> types,
Map<AbstractType<?>, Byte> inverseMapping,
List<V> values,
byte lastEoc)
{
assert types.size() == values.size();
int numComponents = types.size();
// Compute the total number of bytes that we'll need to store the types and their payloads.
int totalLength = 0;
for (int i = 0; i < numComponents; ++i)
{
AbstractType<?> type = types.get(i);
Byte alias = inverseMapping.get(type);
int typeNameLength = alias == null ? type.toString().getBytes(StandardCharsets.UTF_8).length : 0;
// The type data will be stored by means of the type's fully qualified name, not by aliasing, so:
// 1. The type data header should be the fully qualified name length in bytes.
// 2. The length should be small enough so that it fits in 15 bits (2 bytes with the first bit zero).
assert typeNameLength <= 0x7FFF;
int valueLength = accessor.size(values.get(i));
// The value length should also expect its first bit to be 0, as the length should be stored as a signed
// 2-byte value (short).
assert valueLength <= 0x7FFF;
totalLength += 2 + typeNameLength + 2 + valueLength + 1;
}
V result = accessor.allocate(totalLength);
int offset = 0;
for (int i = 0; i < numComponents; ++i)
{
AbstractType<?> type = types.get(i);
Byte alias = inverseMapping.get(type);
if (alias == null)
{
// Write the type data (2-byte length header + the fully qualified type name in UTF-8).
byte[] typeNameBytes = type.toString().getBytes(StandardCharsets.UTF_8);
accessor.putShort(result,
offset,
(short) typeNameBytes.length); // this should work fine also if length >= 32768
offset += 2;
accessor.copyByteArrayTo(typeNameBytes, 0, result, offset, typeNameBytes.length);
offset += typeNameBytes.length;
}
else
{
accessor.putShort(result, offset, (short) (alias | 0x8000));
offset += 2;
}
// Write the type payload data (2-byte length header + the payload).
V value = values.get(i);
int bytesToCopy = accessor.size(value);
accessor.putShort(result, offset, (short) bytesToCopy);
offset += 2;
accessor.copyTo(value, 0, result, accessor, offset, bytesToCopy);
offset += bytesToCopy;
// Write the end-of-component byte.
accessor.putByte(result, offset, i != numComponents - 1 ? (byte) 0 : lastEoc);
offset += 1;
}
return result;
}
protected ParsedComparator parseComparator(int i, String part)
{
return new DynamicParsedComparator(part);
}
protected <V> AbstractType<?> validateComparator(int i, V input, ValueAccessor<V> accessor, int offset) throws MarshalException
{
AbstractType<?> comparator = null;
if (accessor.sizeFromOffset(input, offset) < 2)
throw new MarshalException("Not enough bytes to header of the comparator part of component " + i);
int header = accessor.getShort(input, offset);
offset += TypeSizes.SHORT_SIZE;
if ((header & 0x8000) == 0)
{
if (accessor.sizeFromOffset(input, offset) < header)
throw new MarshalException("Not enough bytes to read comparator name of component " + i);
V value = accessor.slice(input, offset, header);
String valueStr = null;
try
{
valueStr = accessor.toString(value);
comparator = TypeParser.parse(valueStr);
}
catch (CharacterCodingException ce)
{
// ByteBufferUtil.string failed.
// Log it here and we'll further throw an exception below since comparator == null
logger.error("Failed when decoding the byte buffer in ByteBufferUtil.string()", ce);
}
catch (Exception e)
{
// parse failed.
// Log it here and we'll further throw an exception below since comparator == null
logger.error("Failed to parse value string \"{}\" with exception:", valueStr, e);
}
}
else
{
comparator = aliases.get((byte)(header & 0xFF));
}
if (comparator == null)
throw new MarshalException("Cannot find comparator for component " + i);
else
return comparator;
}
public ByteBuffer decompose(Object... objects)
{
throw new UnsupportedOperationException();
}
@Override
public boolean isCompatibleWith(AbstractType<?> previous)
{
if (this == previous)
return true;
if (!(previous instanceof DynamicCompositeType))
return false;
// Adding new aliases is fine (but removing is not)
// Note that modifying the type for an alias to a compatible type is
// *not* fine since this would deal correctly with mixed aliased/not
// aliased component.
DynamicCompositeType cp = (DynamicCompositeType)previous;
if (aliases.size() < cp.aliases.size())
return false;
for (Map.Entry<Byte, AbstractType<?>> entry : cp.aliases.entrySet())
{
AbstractType<?> tprev = entry.getValue();
AbstractType<?> tnew = aliases.get(entry.getKey());
if (tnew == null || tnew != tprev)
return false;
}
return true;
}
@Override
public <V> boolean referencesUserType(V name, ValueAccessor<V> accessor)
{
return any(aliases.values(), t -> t.referencesUserType(name, accessor));
}
@Override
public DynamicCompositeType withUpdatedUserType(UserType udt)
{
if (!referencesUserType(udt.name))
return this;
instances.remove(aliases);
return getInstance(Maps.transformValues(aliases, v -> v.withUpdatedUserType(udt)));
}
@Override
public AbstractType<?> expandUserTypes()
{
return getInstance(Maps.transformValues(aliases, v -> v.expandUserTypes()));
}
private class DynamicParsedComparator implements ParsedComparator
{
final AbstractType<?> type;
final boolean isAlias;
final String comparatorName;
final String remainingPart;
DynamicParsedComparator(String part)
{
String[] splits = part.split("@");
if (splits.length != 2)
throw new IllegalArgumentException("Invalid component representation: " + part);
comparatorName = splits[0];
remainingPart = splits[1];
try
{
AbstractType<?> t = null;
if (comparatorName.length() == 1)
{
// try for an alias
// Note: the char to byte cast is theorically bogus for unicode character. I take full
// responsibility if someone get hit by this (without making it on purpose)
t = aliases.get((byte)comparatorName.charAt(0));
}
isAlias = t != null;
if (!isAlias)
{
t = TypeParser.parse(comparatorName);
}
type = t;
}
catch (SyntaxException | ConfigurationException e)
{
throw new IllegalArgumentException(e);
}
}
public AbstractType<?> getAbstractType()
{
return type;
}
public String getRemainingPart()
{
return remainingPart;
}
public int getComparatorSerializedSize()
{
return isAlias ? 2 : 2 + ByteBufferUtil.bytes(comparatorName).remaining();
}
public void serializeComparator(ByteBuffer bb)
{
int header = 0;
if (isAlias)
header = 0x8000 | (((byte)comparatorName.charAt(0)) & 0xFF);
else
header = comparatorName.length();
ByteBufferUtil.writeShortLength(bb, header);
if (!isAlias)
bb.put(ByteBufferUtil.bytes(comparatorName));
}
}
@Override
public String toString()
{
return getClass().getName() + TypeParser.stringifyAliasesParameters(aliases);
}
/*
* A comparator that always sorts it's first argument before the second
* one.
*/
private static class FixedValueComparator extends AbstractType<Void>
{
public static final FixedValueComparator alwaysLesserThan = new FixedValueComparator(-1);
public static final FixedValueComparator alwaysGreaterThan = new FixedValueComparator(1);
private final int cmp;
public FixedValueComparator(int cmp)
{
super(ComparisonType.CUSTOM);
this.cmp = cmp;
}
public <VL, VR> int compareCustom(VL left, ValueAccessor<VL> accessorL, VR right, ValueAccessor<VR> accessorR)
{
return cmp;
}
@Override
public <V> Void compose(V value, ValueAccessor<V> accessor)
{
throw new UnsupportedOperationException();
}
public ByteBuffer decompose(Void value)
{
throw new UnsupportedOperationException();
}
public <V> String getString(V value, ValueAccessor<V> accessor)
{
throw new UnsupportedOperationException();
}
public ByteBuffer fromString(String str)
{
throw new UnsupportedOperationException();
}
@Override
public Term fromJSONObject(Object parsed)
{
throw new UnsupportedOperationException();
}
@Override
public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion)
{
throw new UnsupportedOperationException();
}
@Override
public void validate(ByteBuffer bytes)
{
throw new UnsupportedOperationException();
}
public TypeSerializer<Void> getSerializer()
{
throw new UnsupportedOperationException();
}
}
}