/* * 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.*; import com.google.common.base.Objects; import org.apache.cassandra.cql3.*; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.*; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; /** * A user defined type. * * A user type is really just a tuple type on steroids. */ public class UserType extends TupleType { public final String keyspace; public final ByteBuffer name; private final List<ByteBuffer> fieldNames; private final List<String> stringFieldNames; public UserType(String keyspace, ByteBuffer name, List<ByteBuffer> fieldNames, List<AbstractType<?>> fieldTypes) { super(fieldTypes); assert fieldNames.size() == fieldTypes.size(); this.keyspace = keyspace; this.name = name; this.fieldNames = fieldNames; this.stringFieldNames = new ArrayList<>(fieldNames.size()); for (ByteBuffer fieldName : fieldNames) { try { stringFieldNames.add(ByteBufferUtil.string(fieldName, StandardCharsets.UTF_8)); } catch (CharacterCodingException ex) { throw new AssertionError("Got non-UTF8 field name for user-defined type: " + ByteBufferUtil.bytesToHex(fieldName), ex); } } } 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<ByteBuffer> columnNames = new ArrayList<>(params.right.size()); List<AbstractType<?>> columnTypes = new ArrayList<>(params.right.size()); for (Pair<ByteBuffer, AbstractType> p : params.right) { columnNames.add(p.left); columnTypes.add(p.right.freeze()); } return new UserType(keyspace, name, columnNames, columnTypes); } public AbstractType<?> fieldType(int i) { return type(i); } public List<AbstractType<?>> fieldTypes() { return types; } public ByteBuffer fieldName(int i) { return fieldNames.get(i); } public String fieldNameAsString(int i) { return stringFieldNames.get(i); } public List<ByteBuffer> fieldNames() { return fieldNames; } public String getNameAsString() { return UTF8Type.instance.compose(name); } // Note: the only reason we override this is to provide nicer error message, but since that's not that much code... @Override public void validate(ByteBuffer bytes) throws MarshalException { ByteBuffer input = bytes.duplicate(); for (int i = 0; i < size(); i++) { // we allow the input to have less fields than declared so as to support field addition. if (!input.hasRemaining()) return; if (input.remaining() < 4) throw new MarshalException(String.format("Not enough bytes to read size of %dth field %s", i, fieldName(i))); int size = input.getInt(); // size < 0 means null value if (size < 0) continue; if (input.remaining() < size) throw new MarshalException(String.format("Not enough bytes to read %dth field %s", i, fieldName(i))); ByteBuffer field = ByteBufferUtil.readBytes(input, size); types.get(i).validate(field); } // We're allowed to get less fields than declared, but not more if (input.hasRemaining()) throw new MarshalException("Invalid remaining data after end of UDT 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, int 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 int hashCode() { return Objects.hashCode(keyspace, name, fieldNames, types); } @Override public boolean equals(Object o) { if(!(o instanceof UserType)) return false; UserType that = (UserType)o; return keyspace.equals(that.keyspace) && name.equals(that.name) && fieldNames.equals(that.fieldNames) && types.equals(that.types); } @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 String toString() { return getClass().getName() + TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types); } }