blob: cd321200071799e3bec9baf5c4ab3e4932a33f41 [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.ignite.internal.binary;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.binary.BinaryObjectException;
import org.apache.ignite.binary.BinaryReflectiveSerializer;
import org.apache.ignite.binary.BinarySerializer;
import org.apache.ignite.binary.Binarylizable;
import org.apache.ignite.internal.UnregisteredClassException;
import org.apache.ignite.internal.UnregisteredBinaryTypeException;
import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshaller;
import org.apache.ignite.internal.processors.cache.CacheObjectImpl;
import org.apache.ignite.internal.processors.query.QueryUtils;
import org.apache.ignite.internal.util.GridUnsafe;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.marshaller.MarshallerExclusions;
import org.jetbrains.annotations.Nullable;
import static org.apache.ignite.internal.processors.query.QueryUtils.isGeometryClass;
/**
* Binary class descriptor.
*/
public class BinaryClassDescriptor {
/** */
@GridToStringExclude
private final BinaryContext ctx;
/** */
private final Class<?> cls;
/** Configured serializer. */
private final BinarySerializer serializer;
/** Serializer that is passed during BinaryClassDescriptor construction. Can differ from {@link #serializer}. */
private final BinarySerializer initialSerializer;
/** ID mapper. */
private final BinaryInternalMapper mapper;
/** */
private final BinaryWriteMode mode;
/** */
private final boolean userType;
/** */
private final int typeId;
/** */
private final String typeName;
/** Affinity key field name. */
private final String affKeyFieldName;
/** */
private final Constructor<?> ctor;
/** */
private final BinaryFieldAccessor[] fields;
/** Write replacer. */
private final BinaryWriteReplacer writeReplacer;
/** */
private final Method readResolveMtd;
/** */
private final Map<String, BinaryFieldMetadata> stableFieldsMeta;
/** Object schemas. Initialized only for serializable classes and contains only 1 entry. */
private final BinarySchema stableSchema;
/** Schema registry. */
private final BinarySchemaRegistry schemaReg;
/** */
private final boolean registered;
/** */
private final boolean useOptMarshaller;
/** */
private final boolean excluded;
/** */
private final Class<?>[] intfs;
/** Whether stable schema was published. */
private volatile boolean stableSchemaPublished;
/**
* @param ctx Context.
* @param cls Class.
* @param userType User type flag.
* @param typeId Type ID.
* @param typeName Type name.
* @param affKeyFieldName Affinity key field name.
* @param mapper Mapper.
* @param serializer Serializer.
* @param metaDataEnabled Metadata enabled flag.
* @param registered Whether typeId has been successfully registered by MarshallerContext or not.
* @throws BinaryObjectException In case of error.
*/
BinaryClassDescriptor(
BinaryContext ctx,
Class<?> cls,
boolean userType,
int typeId,
String typeName,
@Nullable String affKeyFieldName,
@Nullable BinaryInternalMapper mapper,
@Nullable BinarySerializer serializer,
boolean metaDataEnabled,
boolean registered
) throws BinaryObjectException {
assert ctx != null;
assert cls != null;
assert mapper != null;
initialSerializer = serializer;
// If serializer is not defined at this point, then we have to use OptimizedMarshaller.
useOptMarshaller = serializer == null || isGeometryClass(cls);
// Reset reflective serializer so that we rely on existing reflection-based serialization.
if (serializer instanceof BinaryReflectiveSerializer)
serializer = null;
this.ctx = ctx;
this.cls = cls;
this.typeId = typeId;
this.userType = userType;
this.typeName = typeName;
this.affKeyFieldName = affKeyFieldName;
this.serializer = serializer;
this.mapper = mapper;
this.registered = registered;
schemaReg = ctx.schemaRegistry(typeId);
excluded = MarshallerExclusions.isExcluded(cls);
if (excluded)
mode = BinaryWriteMode.EXCLUSION;
else if (useOptMarshaller)
mode = BinaryWriteMode.OPTIMIZED; // Will not be used anywhere.
else {
if (cls == BinaryEnumObjectImpl.class)
mode = BinaryWriteMode.BINARY_ENUM;
else
mode = serializer != null ? BinaryWriteMode.BINARY : BinaryUtils.mode(cls);
}
if (useOptMarshaller && userType && !U.isIgnite(cls) && !U.isJdk(cls) && !QueryUtils.isGeometryClass(cls)) {
U.warnDevOnly(ctx.log(), "Class \"" + cls.getName() + "\" cannot be serialized using " +
BinaryMarshaller.class.getSimpleName() + " because it either implements Externalizable interface " +
"or have writeObject/readObject methods. " + OptimizedMarshaller.class.getSimpleName() + " will be " +
"used instead and class instances will be deserialized on the server. Please ensure that all nodes " +
"have this class in classpath. To enable binary serialization either implement " +
Binarylizable.class.getSimpleName() + " interface or set explicit serializer using " +
"BinaryTypeConfiguration.setSerializer() method.");
}
switch (mode) {
case P_BYTE:
case P_BOOLEAN:
case P_SHORT:
case P_CHAR:
case P_INT:
case P_LONG:
case P_FLOAT:
case P_DOUBLE:
case BYTE:
case SHORT:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case CHAR:
case BOOLEAN:
case DECIMAL:
case STRING:
case UUID:
case DATE:
case TIMESTAMP:
case TIME:
case BYTE_ARR:
case SHORT_ARR:
case INT_ARR:
case LONG_ARR:
case FLOAT_ARR:
case DOUBLE_ARR:
case CHAR_ARR:
case BOOLEAN_ARR:
case DECIMAL_ARR:
case STRING_ARR:
case UUID_ARR:
case DATE_ARR:
case TIMESTAMP_ARR:
case TIME_ARR:
case OBJECT_ARR:
case COL:
case MAP:
case BINARY_OBJ:
case ENUM:
case BINARY_ENUM:
case ENUM_ARR:
case CLASS:
case OPTIMIZED:
case EXCLUSION:
ctor = null;
fields = null;
stableFieldsMeta = null;
stableSchema = null;
intfs = null;
break;
case PROXY:
ctor = null;
fields = null;
stableFieldsMeta = null;
stableSchema = null;
intfs = cls.getInterfaces();
break;
case BINARY:
ctor = constructor(cls);
fields = null;
stableFieldsMeta = null;
stableSchema = null;
intfs = null;
break;
case OBJECT:
// Must not use constructor to honor transient fields semantics.
ctor = null;
Map<Object, BinaryFieldAccessor> fields0;
if (BinaryUtils.FIELDS_SORTED_ORDER) {
fields0 = new TreeMap<>();
stableFieldsMeta = metaDataEnabled ? new TreeMap<String, BinaryFieldMetadata>() : null;
}
else {
fields0 = new LinkedHashMap<>();
stableFieldsMeta = metaDataEnabled ? new LinkedHashMap<String, BinaryFieldMetadata>() : null;
}
Set<String> duplicates = duplicateFields(cls);
Collection<String> names = new HashSet<>();
Collection<Integer> ids = new HashSet<>();
for (Class<?> c = cls; c != null && !c.equals(Object.class); c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (serializeField(f)) {
f.setAccessible(true);
String name = f.getName();
if (duplicates.contains(name))
name = BinaryUtils.qualifiedFieldName(c, name);
boolean added = names.add(name);
assert added : name;
int fieldId = this.mapper.fieldId(typeId, name);
if (!ids.add(fieldId))
throw new BinaryObjectException("Duplicate field ID: " + name);
BinaryFieldAccessor fieldInfo = BinaryFieldAccessor.create(f, fieldId);
fields0.put(name, fieldInfo);
if (metaDataEnabled)
stableFieldsMeta.put(name, new BinaryFieldMetadata(fieldInfo));
}
}
}
fields = fields0.values().toArray(new BinaryFieldAccessor[fields0.size()]);
BinarySchema.Builder schemaBuilder = BinarySchema.Builder.newBuilder();
for (BinaryFieldAccessor field : fields)
schemaBuilder.addField(field.id);
stableSchema = schemaBuilder.build();
intfs = null;
break;
default:
// Should never happen.
throw new BinaryObjectException("Invalid mode: " + mode);
}
BinaryWriteReplacer writeReplacer0 = BinaryUtils.writeReplacer(cls);
Method writeReplaceMthd;
if (mode == BinaryWriteMode.BINARY || mode == BinaryWriteMode.OBJECT) {
readResolveMtd = U.findInheritableMethod(cls, "readResolve");
writeReplaceMthd = U.findInheritableMethod(cls, "writeReplace");
}
else {
readResolveMtd = null;
writeReplaceMthd = null;
}
if (writeReplaceMthd != null && writeReplacer0 == null)
writeReplacer0 = new BinaryMethodWriteReplacer(writeReplaceMthd);
writeReplacer = writeReplacer0;
}
/**
* Find all fields with duplicate names in the class.
*
* @param cls Class.
* @return Fields with duplicate names.
*/
private static Set<String> duplicateFields(Class cls) {
Set<String> all = new HashSet<>();
Set<String> duplicates = new HashSet<>();
for (Class<?> c = cls; c != null && !c.equals(Object.class); c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (serializeField(f)) {
String name = f.getName();
if (!all.add(name))
duplicates.add(name);
}
}
}
return duplicates;
}
/**
* Whether the field must be serialized.
*
* @param f Field.
* @return {@code True} if must be serialized.
*/
private static boolean serializeField(Field f) {
int mod = f.getModifiers();
return !Modifier.isStatic(mod) && !Modifier.isTransient(mod);
}
/**
* @return {@code True} if enum.
*/
boolean isEnum() {
return mode == BinaryWriteMode.ENUM;
}
/**
* @return Described class.
*/
Class<?> describedClass() {
return cls;
}
/**
* @return Type ID.
*/
public int typeId() {
return typeId;
}
/**
* @return Type name.
*/
String typeName() {
return typeName;
}
/**
* @return Type mapper.
*/
BinaryInternalMapper mapper() {
return mapper;
}
/**
* @return Serializer.
*/
BinarySerializer serializer() {
return serializer;
}
/**
* @return Initial serializer that is passed during BinaryClassDescriptor construction.
* Can differ from {@link #serializer}.
*/
BinarySerializer initialSerializer() {
return initialSerializer;
}
/**
* @return Affinity field key name.
*/
String affFieldKeyName() {
return affKeyFieldName;
}
/**
* @return User type flag.
*/
boolean userType() {
return userType;
}
/**
* @return Fields meta data.
*/
Map<String, BinaryFieldMetadata> fieldsMeta() {
return stableFieldsMeta;
}
/**
* @return Schema.
*/
BinarySchema schema() {
return stableSchema;
}
/**
* @return Whether typeId has been successfully registered by MarshallerContext or not.
*/
public boolean registered() {
return registered;
}
/**
* @return {@code true} if {@link OptimizedMarshaller} must be used instead of {@link BinaryMarshaller}
* for object serialization and deserialization.
*/
public boolean useOptimizedMarshaller() {
return useOptMarshaller;
}
/**
* Checks whether the class values are explicitly excluded from marshalling.
*
* @return {@code true} if excluded, {@code false} otherwise.
*/
public boolean excluded() {
return excluded;
}
/**
* @return {@code True} if write-replace should be performed for class.
*/
public boolean isWriteReplace() {
return writeReplacer != null;
}
/**
* Perform write replace.
*
* @param obj Original object.
* @return Replaced object.
*/
public Object writeReplace(Object obj) {
assert isWriteReplace();
return writeReplacer.replace(obj);
}
/**
* Register current stable schema if applicable.
*/
public void registerStableSchema() {
if (schemaReg != null && stableSchema != null) {
int schemaId = stableSchema.schemaId();
if (schemaReg.schema(schemaId) == null)
schemaReg.addSchema(stableSchema.schemaId(), stableSchema);
}
}
/**
* @return binaryReadResolve() method
*/
@SuppressWarnings("UnusedDeclaration")
@Nullable Method getReadResolveMethod() {
return readResolveMtd;
}
/**
* @param obj Object.
* @param writer Writer.
* @throws BinaryObjectException In case of error.
*/
void write(Object obj, BinaryWriterExImpl writer) throws BinaryObjectException {
try {
assert obj != null;
assert writer != null;
assert mode != BinaryWriteMode.OPTIMIZED : "OptimizedMarshaller should not be used here: " + cls.getName();
writer.typeId(typeId);
switch (mode) {
case P_BYTE:
case BYTE:
writer.writeByteFieldPrimitive((byte)obj);
break;
case P_SHORT:
case SHORT:
writer.writeShortFieldPrimitive((short)obj);
break;
case P_INT:
case INT:
writer.writeIntFieldPrimitive((int)obj);
break;
case P_LONG:
case LONG:
writer.writeLongFieldPrimitive((long)obj);
break;
case P_FLOAT:
case FLOAT:
writer.writeFloatFieldPrimitive((float)obj);
break;
case P_DOUBLE:
case DOUBLE:
writer.writeDoubleFieldPrimitive((double)obj);
break;
case P_CHAR:
case CHAR:
writer.writeCharFieldPrimitive((char)obj);
break;
case P_BOOLEAN:
case BOOLEAN:
writer.writeBooleanFieldPrimitive((boolean)obj);
break;
case DECIMAL:
writer.doWriteDecimal((BigDecimal)obj);
break;
case STRING:
writer.doWriteString((String)obj);
break;
case UUID:
writer.doWriteUuid((UUID)obj);
break;
case DATE:
writer.doWriteDate((Date)obj);
break;
case TIMESTAMP:
writer.doWriteTimestamp((Timestamp)obj);
break;
case TIME:
writer.doWriteTime((Time)obj);
break;
case BYTE_ARR:
writer.doWriteByteArray((byte[])obj);
break;
case SHORT_ARR:
writer.doWriteShortArray((short[])obj);
break;
case INT_ARR:
writer.doWriteIntArray((int[])obj);
break;
case LONG_ARR:
writer.doWriteLongArray((long[])obj);
break;
case FLOAT_ARR:
writer.doWriteFloatArray((float[])obj);
break;
case DOUBLE_ARR:
writer.doWriteDoubleArray((double[])obj);
break;
case CHAR_ARR:
writer.doWriteCharArray((char[])obj);
break;
case BOOLEAN_ARR:
writer.doWriteBooleanArray((boolean[])obj);
break;
case DECIMAL_ARR:
writer.doWriteDecimalArray((BigDecimal[])obj);
break;
case STRING_ARR:
writer.doWriteStringArray((String[])obj);
break;
case UUID_ARR:
writer.doWriteUuidArray((UUID[])obj);
break;
case DATE_ARR:
writer.doWriteDateArray((Date[])obj);
break;
case TIMESTAMP_ARR:
writer.doWriteTimestampArray((Timestamp[])obj);
break;
case TIME_ARR:
writer.doWriteTimeArray((Time[])obj);
break;
case OBJECT_ARR:
writer.doWriteObjectArray((Object[])obj);
break;
case COL:
writer.doWriteCollection((Collection<?>)obj);
break;
case MAP:
writer.doWriteMap((Map<?, ?>)obj);
break;
case ENUM:
writer.doWriteEnum((Enum<?>)obj);
break;
case BINARY_ENUM:
writer.doWriteBinaryEnum((BinaryEnumObjectImpl)obj);
break;
case ENUM_ARR:
writer.doWriteEnumArray((Object[])obj);
break;
case CLASS:
writer.doWriteClass((Class)obj);
break;
case PROXY:
writer.doWriteProxy((Proxy)obj, intfs);
break;
case BINARY_OBJ:
writer.doWriteBinaryObject((BinaryObjectImpl)obj);
break;
case BINARY:
if (preWrite(writer, obj)) {
try {
if (serializer != null)
serializer.writeBinary(obj, writer);
else
((Binarylizable)obj).writeBinary(writer);
postWrite(writer);
// Check whether we need to update metadata.
// The reason for this check is described in https://issues.apache.org/jira/browse/IGNITE-7138.
if (obj.getClass() != BinaryMetadata.class && obj.getClass() != BinaryTreeMap.class) {
int schemaId = writer.schemaId();
if (schemaReg.schema(schemaId) == null) {
// This is new schema, let's update metadata.
BinaryMetadataCollector collector =
new BinaryMetadataCollector(typeId, typeName, mapper);
if (serializer != null)
serializer.writeBinary(obj, collector);
else
((Binarylizable)obj).writeBinary(collector);
BinarySchema newSchema = collector.schema();
BinaryMetadata meta = new BinaryMetadata(typeId, typeName, collector.meta(),
affKeyFieldName, Collections.singleton(newSchema), false, null);
ctx.updateMetadata(typeId, meta, writer.failIfUnregistered());
schemaReg.addSchema(newSchema.schemaId(), newSchema);
}
}
postWriteHashCode(writer, obj);
}
finally {
writer.popSchema();
}
}
break;
case OBJECT:
if (userType && !stableSchemaPublished) {
// Update meta before write object with new schema
BinaryMetadata meta = new BinaryMetadata(typeId, typeName, stableFieldsMeta,
affKeyFieldName, Collections.singleton(stableSchema), false, null);
ctx.updateMetadata(typeId, meta, writer.failIfUnregistered());
schemaReg.addSchema(stableSchema.schemaId(), stableSchema);
stableSchemaPublished = true;
}
if (preWrite(writer, obj)) {
try {
for (BinaryFieldAccessor info : fields)
info.write(obj, writer);
writer.schemaId(stableSchema.schemaId());
postWrite(writer);
postWriteHashCode(writer, obj);
}
finally {
writer.popSchema();
}
}
break;
default:
assert false : "Invalid mode: " + mode;
}
}
catch (Exception e) {
if (e instanceof UnregisteredBinaryTypeException || e instanceof UnregisteredClassException)
throw e;
String msg;
if (S.INCLUDE_SENSITIVE && !F.isEmpty(typeName))
msg = "Failed to serialize object [typeName=" + typeName + ']';
else
msg = "Failed to serialize object [typeId=" + typeId + ']';
U.error(ctx.log(), msg, e);
throw new BinaryObjectException(msg, e);
}
}
/**
* @param reader Reader.
* @return Object.
* @throws BinaryObjectException If failed.
*/
Object read(BinaryReaderExImpl reader) throws BinaryObjectException {
try {
assert reader != null;
assert mode != BinaryWriteMode.OPTIMIZED : "OptimizedMarshaller should not be used here: " + cls.getName();
Object res;
switch (mode) {
case BINARY:
res = newInstance();
reader.setHandle(res);
if (serializer != null)
serializer.readBinary(res, reader);
else
((Binarylizable)res).readBinary(reader);
break;
case OBJECT:
res = newInstance();
reader.setHandle(res);
for (BinaryFieldAccessor info : fields)
info.read(res, reader);
break;
default:
assert false : "Invalid mode: " + mode;
return null;
}
if (readResolveMtd != null) {
try {
res = readResolveMtd.invoke(res);
reader.setHandle(res);
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
if (e.getTargetException() instanceof BinaryObjectException)
throw (BinaryObjectException)e.getTargetException();
throw new BinaryObjectException("Failed to execute readResolve() method on " + res, e);
}
}
return res;
}
catch (Exception e) {
String msg;
if (S.INCLUDE_SENSITIVE && !F.isEmpty(typeName))
msg = "Failed to deserialize object [typeName=" + typeName + ']';
else
msg = "Failed to deserialize object [typeId=" + typeId + ']';
U.error(ctx.log(), msg, e);
throw new BinaryObjectException(msg, e);
}
}
/**
* Pre-write phase.
*
* @param writer Writer.
* @param obj Object.
* @return Whether further write is needed.
*/
private boolean preWrite(BinaryWriterExImpl writer, Object obj) {
if (writer.tryWriteAsHandle(obj))
return false;
writer.preWrite(registered ? null : cls.getName());
return true;
}
/**
* Post-write phase.
*
* @param writer Writer.
*/
private void postWrite(BinaryWriterExImpl writer) {
writer.postWrite(userType, registered);
}
/**
* Post-write routine for hash code.
*
* @param writer Writer.
* @param obj Object.
*/
private void postWriteHashCode(BinaryWriterExImpl writer, Object obj) {
// No need to call "postWriteHashCode" here because we do not care about hash code.
if (!(obj instanceof CacheObjectImpl))
writer.postWriteHashCode(registered ? null : cls.getName());
}
/**
* @return Instance.
* @throws BinaryObjectException In case of error.
*/
private Object newInstance() throws BinaryObjectException {
try {
return ctor != null ? ctor.newInstance() : GridUnsafe.allocateInstance(cls);
}
catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new BinaryObjectException("Failed to instantiate instance: " + cls, e);
}
}
/**
* @param cls Class.
* @return Constructor.
* @throws BinaryObjectException If constructor doesn't exist.
*/
@SuppressWarnings("ConstantConditions")
@Nullable private static Constructor<?> constructor(Class<?> cls) throws BinaryObjectException {
assert cls != null;
try {
Constructor<?> ctor = U.forceEmptyConstructor(cls);
if (ctor == null)
throw new BinaryObjectException("Failed to find empty constructor for class: " + cls.getName());
ctor.setAccessible(true);
return ctor;
}
catch (IgniteCheckedException e) {
throw new BinaryObjectException("Failed to get constructor for class: " + cls.getName(), e);
}
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(BinaryClassDescriptor.class, this);
}
}