/*
 * 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.util.*;
import java.util.stream.Collectors;

import com.google.common.base.Objects;

import org.apache.cassandra.cql3.*;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.serializers.TypeSerializer;
import org.apache.cassandra.serializers.UserTypeSerializer;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

A user defined type. A user type is really just a tuple type on steroids.
/** * A user defined type. * * A user type is really just a tuple type on steroids. */
public class UserType extends TupleType { private static final Logger logger = LoggerFactory.getLogger(UserType.class); public final String keyspace; public final ByteBuffer name; private final List<FieldIdentifier> fieldNames; private final List<String> stringFieldNames; private final boolean isMultiCell; private final UserTypeSerializer serializer; public UserType(String keyspace, ByteBuffer name, List<FieldIdentifier> fieldNames, List<AbstractType<?>> fieldTypes, boolean isMultiCell) { super(fieldTypes, false); assert fieldNames.size() == fieldTypes.size(); this.keyspace = keyspace; this.name = name; this.fieldNames = fieldNames; this.stringFieldNames = new ArrayList<>(fieldNames.size()); this.isMultiCell = isMultiCell; LinkedHashMap<String , TypeSerializer<?>> fieldSerializers = new LinkedHashMap<>(fieldTypes.size()); for (int i = 0, m = fieldNames.size(); i < m; i++) { String stringFieldName = fieldNames.get(i).toString(); stringFieldNames.add(stringFieldName); fieldSerializers.put(stringFieldName, fieldTypes.get(i).getSerializer()); } this.serializer = new UserTypeSerializer(fieldSerializers); } public static UserType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException { Pair<Pair<String, ByteBuffer>, List<Pair<ByteBuffer, AbstractType>>> params = parser.getUserTypeParameters(); String keyspace = params.left.left; ByteBuffer name = params.left.right; List<FieldIdentifier> columnNames = new ArrayList<>(params.right.size()); List<AbstractType<?>> columnTypes = new ArrayList<>(params.right.size()); for (Pair<ByteBuffer, AbstractType> p : params.right) { columnNames.add(new FieldIdentifier(p.left)); columnTypes.add(p.right); } return new UserType(keyspace, name, columnNames, columnTypes, true); } @Override public boolean isUDT() { return true; } @Override public boolean isMultiCell() { return isMultiCell; } @Override public boolean isFreezable() { return true; } public AbstractType<?> fieldType(int i) { return type(i); } public List<AbstractType<?>> fieldTypes() { return types; } public FieldIdentifier fieldName(int i) { return fieldNames.get(i); } public String fieldNameAsString(int i) { return stringFieldNames.get(i); } public List<FieldIdentifier> fieldNames() { return fieldNames; } public String getNameAsString() { return UTF8Type.instance.compose(name); } public int fieldPosition(FieldIdentifier fieldName) { return fieldNames.indexOf(fieldName); } public CellPath cellPathForField(FieldIdentifier fieldName) { // we use the field position instead of the field name to allow for field renaming in ALTER TYPE statements return CellPath.create(ByteBufferUtil.bytes((short)fieldPosition(fieldName))); } public ShortType nameComparator() { return ShortType.instance; } public ByteBuffer serializeForNativeProtocol(Iterator<Cell> cells, ProtocolVersion protocolVersion) { assert isMultiCell; ByteBuffer[] components = new ByteBuffer[size()]; short fieldPosition = 0; while (cells.hasNext()) { Cell cell = cells.next(); // handle null fields that aren't at the end short fieldPositionOfCell = ByteBufferUtil.toShort(cell.path().get(0)); while (fieldPosition < fieldPositionOfCell) components[fieldPosition++] = null; components[fieldPosition++] = cell.value(); } // append trailing nulls for missing cells while (fieldPosition < size()) components[fieldPosition++] = null; return TupleType.buildValue(components); } public void validateCell(Cell cell) throws MarshalException { if (isMultiCell) { ByteBuffer path = cell.path().get(0); nameComparator().validate(path); Short fieldPosition = nameComparator().getSerializer().deserialize(path); fieldType(fieldPosition).validate(cell.value()); } else { validate(cell.value()); } } @Override public Term fromJSONObject(Object parsed) throws MarshalException { if (parsed instanceof String) parsed = Json.decodeJson((String) parsed); if (!(parsed instanceof Map)) throw new MarshalException(String.format( "Expected a map, but got a %s: %s", parsed.getClass().getSimpleName(), parsed)); Map<String, Object> map = (Map<String, Object>) parsed; Json.handleCaseSensitivity(map); List<Term> terms = new ArrayList<>(types.size()); Set keys = map.keySet(); assert keys.isEmpty() || keys.iterator().next() instanceof String; int foundValues = 0; for (int i = 0; i < types.size(); i++) { Object value = map.get(stringFieldNames.get(i)); if (value == null) { terms.add(Constants.NULL_VALUE); } else { terms.add(types.get(i).fromJSONObject(value)); foundValues += 1; } } // check for extra, unrecognized fields if (foundValues != map.size()) { for (Object fieldName : keys) { if (!stringFieldNames.contains(fieldName)) throw new MarshalException(String.format( "Unknown field '%s' in value of user defined type %s", fieldName, getNameAsString())); } } return new UserTypes.DelayedValue(this, terms); } @Override public String toJSONString(ByteBuffer buffer, ProtocolVersion protocolVersion) { ByteBuffer[] buffers = split(buffer); StringBuilder sb = new StringBuilder("{"); for (int i = 0; i < types.size(); i++) { if (i > 0) sb.append(", "); String name = stringFieldNames.get(i); if (!name.equals(name.toLowerCase(Locale.US))) name = "\"" + name + "\""; sb.append('"'); sb.append(Json.quoteAsJsonString(name)); sb.append("\": "); ByteBuffer valueBuffer = (i >= buffers.length) ? null : buffers[i]; if (valueBuffer == null) sb.append("null"); else sb.append(types.get(i).toJSONString(valueBuffer, protocolVersion)); } return sb.append("}").toString(); } @Override public UserType freeze() { if (isMultiCell) return new UserType(keyspace, name, fieldNames, fieldTypes(), false); else return this; } @Override public AbstractType<?> freezeNestedMulticellTypes() { if (!isMultiCell()) return this; // the behavior here doesn't exactly match the method name: we want to freeze everything inside of UDTs List<AbstractType<?>> newTypes = fieldTypes().stream() .map(subtype -> (subtype.isFreezable() && subtype.isMultiCell() ? subtype.freeze() : subtype)) .collect(Collectors.toList()); return new UserType(keyspace, name, fieldNames, newTypes, isMultiCell); } @Override public int hashCode() { return Objects.hashCode(keyspace, name, fieldNames, types, isMultiCell); } @Override public boolean isValueCompatibleWith(AbstractType<?> previous) { if (this == previous) return true; if (!(previous instanceof UserType)) return false; UserType other = (UserType) previous; if (isMultiCell != other.isMultiCell()) return false; if (!keyspace.equals(other.keyspace)) return false; Iterator<AbstractType<?>> thisTypeIter = types.iterator(); Iterator<AbstractType<?>> previousTypeIter = other.types.iterator(); while (thisTypeIter.hasNext() && previousTypeIter.hasNext()) { if (!thisTypeIter.next().isCompatibleWith(previousTypeIter.next())) return false; } // it's okay for the new type to have additional fields, but not for the old type to have additional fields return !previousTypeIter.hasNext(); } @Override public boolean equals(Object o) { return o instanceof UserType && equals(o, false); } @Override public boolean equals(Object o, boolean ignoreFreezing) { if(!(o instanceof UserType)) return false; UserType that = (UserType)o; if (!keyspace.equals(that.keyspace) || !name.equals(that.name) || !fieldNames.equals(that.fieldNames)) return false; if (!ignoreFreezing && isMultiCell != that.isMultiCell) return false; if (this.types.size() != that.types.size()) return false; Iterator<AbstractType<?>> otherTypeIter = that.types.iterator(); for (AbstractType<?> type : types) { if (!type.equals(otherTypeIter.next(), ignoreFreezing)) return false; } return true; } @Override public CQL3Type asCQL3Type() { return CQL3Type.UserDefined.create(this); } @Override public boolean referencesUserType(String userTypeName) { return getNameAsString().equals(userTypeName) || fieldTypes().stream().anyMatch(f -> f.referencesUserType(userTypeName)); } @Override public boolean referencesDuration() { return fieldTypes().stream().anyMatch(f -> f.referencesDuration()); } @Override public String toString() { return this.toString(false); } @Override public boolean isTuple() { return false; } @Override public String toString(boolean ignoreFreezing) { boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); StringBuilder sb = new StringBuilder(); if (includeFrozenType) sb.append(FrozenType.class.getName()).append("("); sb.append(getClass().getName()); sb.append(TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types, ignoreFreezing || !isMultiCell)); if (includeFrozenType) sb.append(")"); return sb.toString(); } @Override public TypeSerializer<ByteBuffer> getSerializer() { return serializer; } }