diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java index 4c075d2823..fbfc1e9741 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java @@ -23,6 +23,7 @@ import static org.apache.fory.type.TypeUtils.OBJECT_TYPE; import static org.apache.fory.type.TypeUtils.STRING_TYPE; +import java.lang.reflect.Field; import java.lang.reflect.Member; import java.util.Collection; import java.util.Map; @@ -44,7 +45,9 @@ import org.apache.fory.serializer.ObjectSerializer; import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.Serializers; +import org.apache.fory.serializer.converter.FieldConverter; import org.apache.fory.type.Descriptor; +import org.apache.fory.type.DescriptorBuilder; import org.apache.fory.type.DescriptorGrouper; import org.apache.fory.util.DefaultValueUtils; import org.apache.fory.util.ExceptionUtils; @@ -221,11 +224,25 @@ protected Expression createRecord(SortedMap recordComponent @Override protected Expression setFieldValue(Expression bean, Descriptor descriptor, Expression value) { if (descriptor.getField() == null) { + FieldConverter converter = descriptor.getFieldConverter(); + if (converter != null) { + Field field = converter.getField(); + StaticInvoke converted = + new StaticInvoke( + converter.getClass(), "convertFrom", TypeRef.of(field.getType()), value); + Descriptor newDesc = + new DescriptorBuilder(descriptor) + .field(field) + .type(field.getType()) + .typeRef(TypeRef.of(field.getType())) + .build(); + return super.setFieldValue(bean, newDesc, converted); + } // Field doesn't exist in current class, skip set this field value. // Note that the field value shouldn't be an inlined value, otherwise field value read may // be ignored. // Add an ignored call here to make expression type to void. - return new Expression.StaticInvoke(ExceptionUtils.class, "ignore", value); + return new StaticInvoke(ExceptionUtils.class, "ignore", value); } return super.setFieldValue(bean, descriptor, value); } diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java index 1e5b14bc6f..7a2bccdbe8 100644 --- a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java +++ b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java @@ -58,6 +58,8 @@ import org.apache.fory.resolver.XtypeResolver; import org.apache.fory.serializer.CompatibleSerializer; import org.apache.fory.serializer.NonexistentClass; +import org.apache.fory.serializer.converter.FieldConverter; +import org.apache.fory.serializer.converter.FieldConverters; import org.apache.fory.type.Descriptor; import org.apache.fory.type.FinalObjectTypeStub; import org.apache.fory.type.GenericType; @@ -276,6 +278,11 @@ public List getDescriptors(TypeResolver resolver, Class cls) { descriptor = descriptor.copyWithTypeName(newDesc.getTypeName()); descriptors.add(descriptor); } else { + FieldConverter converter = + FieldConverters.getConverter(rawType, descriptor.getField()); + if (converter != null) { + newDesc.setFieldConverter(converter); + } descriptors.add(newDesc); } } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java b/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java index c13c9adb07..ca2a8bafcf 100644 --- a/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java +++ b/java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java @@ -23,11 +23,15 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.ToDoubleFunction; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; +import org.apache.fory.collection.ClassValueCache; +import org.apache.fory.collection.Tuple2; import org.apache.fory.memory.Platform; import org.apache.fory.type.TypeUtils; import org.apache.fory.util.GraalvmSupport; @@ -494,20 +498,39 @@ public Object get(Object obj) { } static class GeneratedAccessor extends FieldAccessor { + private static final ClassValueCache>> + cache = ClassValueCache.newClassKeyCache(8); + private final MethodHandle getter; private final MethodHandle setter; protected GeneratedAccessor(Field field) { super(field, -1); - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(field.getDeclaringClass()); + ConcurrentMap> map; try { - this.getter = - lookup.findGetter(field.getDeclaringClass(), field.getName(), field.getType()); - this.setter = - lookup.findSetter(field.getDeclaringClass(), field.getName(), field.getType()); - } catch (IllegalAccessException | NoSuchFieldException ex) { - throw new RuntimeException(ex); + map = cache.get(field.getDeclaringClass(), ConcurrentHashMap::new); + } catch (Exception e) { + throw new RuntimeException(e); } + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(field.getDeclaringClass()); + Tuple2 tuple2 = + map.computeIfAbsent( + field.getName(), + k -> { + try { + MethodHandle getter = + lookup.findGetter( + field.getDeclaringClass(), field.getName(), field.getType()); + MethodHandle setter = + lookup.findSetter( + field.getDeclaringClass(), field.getName(), field.getType()); + return Tuple2.of(getter, setter); + } catch (IllegalAccessException | NoSuchFieldException ex) { + throw new RuntimeException(ex); + } + }); + getter = tuple2.f0; + setter = tuple2.f1; } @Override diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java index e346822e94..2adcbf333d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java @@ -34,6 +34,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -402,6 +403,12 @@ public static List getFieldsWithoutSuperClasses(Class cls, Set getSortedFields(Class cls, boolean searchParent) { + List fields = getFields(cls, searchParent); + fields.sort(Comparator.comparing(f -> f.getName() + f.getDeclaringClass())); + return fields; + } + /** Get fields values from provided object. */ public static List getFieldValues(Collection fields, Object o) { List results = new ArrayList<>(fields.size()); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java index 82b53321d0..cb52c5bfb3 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java @@ -43,6 +43,7 @@ import org.apache.fory.resolver.ClassResolver; import org.apache.fory.resolver.RefResolver; import org.apache.fory.resolver.TypeResolver; +import org.apache.fory.serializer.converter.FieldConverter; import org.apache.fory.type.Descriptor; import org.apache.fory.type.DescriptorGrouper; import org.apache.fory.type.FinalObjectTypeStub; @@ -990,6 +991,7 @@ public static class InternalFieldInfo { protected final short classId; protected final String qualifiedFieldName; protected final FieldAccessor fieldAccessor; + protected final FieldConverter fieldConverter; protected boolean nullable; protected boolean trackingRef; @@ -998,6 +1000,7 @@ private InternalFieldInfo(Fory fory, Descriptor d, short classId) { this.classId = classId; this.qualifiedFieldName = d.getDeclaringClass() + "." + d.getName(); this.fieldAccessor = d.getField() != null ? FieldAccessor.createAccessor(d.getField()) : null; + fieldConverter = d.getFieldConverter(); ForyField foryField = d.getForyField(); nullable = d.isNullable(); if (fory.trackingRef()) { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java index 56917ccf05..7d34dc9ccf 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java @@ -200,15 +200,19 @@ public T read(MemoryBuffer buffer) { fieldAccessor.putObject(obj, fieldValue); } } else { - // Skip the field value from buffer since it doesn't exist in current class - if (skipPrimitiveFieldValueFailed(fory, fieldInfo.classId, buffer)) { - if (fieldInfo.classInfo == null) { - // TODO(chaokunyang) support registered serializer in peer with ref tracking disabled. - fory.readRef(buffer, classInfoHolder); - } else { - AbstractObjectSerializer.readFinalObjectFieldValue( - binding, refResolver, classResolver, fieldInfo, isFinal, buffer); + if (fieldInfo.fieldConverter == null) { + // Skip the field value from buffer since it doesn't exist in current class + if (skipPrimitiveFieldValueFailed(fory, fieldInfo.classId, buffer)) { + if (fieldInfo.classInfo == null) { + // TODO(chaokunyang) support registered serializer in peer with ref tracking disabled. + fory.readRef(buffer, classInfoHolder); + } else { + AbstractObjectSerializer.readFinalObjectFieldValue( + binding, refResolver, classResolver, fieldInfo, isFinal, buffer); + } } + } else { + compatibleRead(buffer, fieldInfo, isFinal, obj); } } } @@ -228,20 +232,32 @@ public T read(MemoryBuffer buffer) { fieldAccessor.putObject(obj, fieldValue); } } + return obj; + } - // Set default values for missing fields in Scala case classes - if (hasDefaultValues) { - DefaultValueUtils.setDefaultValues(obj, defaultValueFields); + private void compatibleRead( + MemoryBuffer buffer, FinalTypeField fieldInfo, boolean isFinal, Object obj) { + Object fieldValue; + short classId = fieldInfo.classId; + if (classId >= ClassResolver.PRIMITIVE_BOOLEAN_CLASS_ID + && classId <= ClassResolver.PRIMITIVE_DOUBLE_CLASS_ID) { + fieldValue = Serializers.readPrimitiveValue(fory, buffer, classId); + } else { + fieldValue = + AbstractObjectSerializer.readFinalObjectFieldValue( + binding, refResolver, classResolver, fieldInfo, isFinal, buffer); } - - return obj; + fieldInfo.fieldConverter.set(obj, fieldValue); } private T newInstance() { if (!hasDefaultValues) { return newBean(); } - return Platform.newInstance(type); + T obj = Platform.newInstance(type); + // Set default values for missing fields in Scala case classes + DefaultValueUtils.setDefaultValues(obj, defaultValueFields); + return obj; } @Override @@ -284,6 +300,7 @@ private void readFields(MemoryBuffer buffer, Object[] fields) { binding, refResolver, classResolver, fieldInfo, isFinal, buffer); } } + // remapping will handle those extra fields from peers. fields[counter++] = null; } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/converter/FieldConverter.java b/java/fory-core/src/main/java/org/apache/fory/serializer/converter/FieldConverter.java new file mode 100644 index 0000000000..2f7cec62d4 --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/converter/FieldConverter.java @@ -0,0 +1,103 @@ +/* + * 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.fory.serializer.converter; + +import java.lang.reflect.Field; +import org.apache.fory.reflect.FieldAccessor; + +/** + * Abstract base class for field converters that handle type conversions during + * serialization/deserialization. + * + *

Field converters are responsible for converting values from one type to another when setting + * field values. This is particularly useful when dealing with cross-language serialization where + * type mappings may differ between languages, or when handling legacy data with different type + * representations. + * + *

Each converter is associated with a specific field through a {@link FieldAccessor}, which + * provides the mechanism to actually set the converted value on the target object. + * + *

Example usage: + * + *

{@code
+ * // Create a converter for an int field
+ * FieldConverter converter = new IntConverter(fieldAccessor);
+ *
+ * // Convert a string "123" to integer and set it on the target object
+ * converter.set(targetObject, "123");
+ * }
+ * + * @param the target type that this converter produces + * @see FieldConverters for factory methods to create specific converter instances + * @see FieldAccessor for the field access mechanism + */ +public abstract class FieldConverter { + private final FieldAccessor fieldAccessor; + + /** + * Constructs a new FieldConverter with the specified field accessor. + * + * @param fieldAccessor the field accessor that will be used to set converted values on target + * objects + * @throws IllegalArgumentException if fieldAccessor is null + */ + protected FieldConverter(FieldAccessor fieldAccessor) { + this.fieldAccessor = fieldAccessor; + } + + /** + * Converts the given object to the target type. + * + *

This method performs the actual type conversion logic. The implementation should handle null + * values appropriately and throw {@link UnsupportedOperationException} for incompatible types. + * + * @param from the object to convert + * @return the converted object of type T + * @throws UnsupportedOperationException if the source type is not compatible with this converter + * @throws NumberFormatException if converting from String to a numeric type and the string is not + * a valid number + * @throws ArithmeticException if the numeric conversion would result in overflow or underflow + */ + public abstract T convert(Object from); + + /** + * Converts the given object and sets it on the target object's field. + * + *

This is a convenience method that combines conversion and field setting in one operation. It + * first converts the input object using {@link #convert(Object)}, then uses the field accessor to + * set the converted value on the target object. + * + * @param target the target object whose field will be set + * @param from the object to convert and set + * @throws UnsupportedOperationException if the source type is not compatible with this converter + * @throws NumberFormatException if converting from String to a numeric type and the string is not + * a valid number + * @throws ArithmeticException if the numeric conversion would result in overflow or underflow + * @throws IllegalArgumentException if target is null or the field accessor fails + */ + public void set(Object target, Object from) { + T converted = convert(from); + fieldAccessor.set(target, converted); + } + + public Field getField() { + return fieldAccessor.getField(); + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/converter/FieldConverters.java b/java/fory-core/src/main/java/org/apache/fory/serializer/converter/FieldConverters.java new file mode 100644 index 0000000000..87f7cdd5aa --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/converter/FieldConverters.java @@ -0,0 +1,634 @@ +/* + * 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.fory.serializer.converter; + +import com.google.common.collect.ImmutableSet; +import java.lang.reflect.Field; +import java.util.Set; +import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.type.TypeUtils; + +/** + * Factory class for creating field converters that handle type conversions between different data + * types. This class provides converters for primitive types and their boxed counterparts, enabling + * automatic type conversion during serialization/deserialization processes. + */ +public class FieldConverters { + + /** + * Creates an appropriate field converter based on the target field type and source object type. + * + * @param from the source object type to convert from + * @param field the target field to convert to + * @return a FieldConverter instance that can handle the conversion, or null if no compatible + * converter exists + */ + public static FieldConverter getConverter(Class from, Field field) { + Class to = field.getType(); + from = TypeUtils.wrap(from); + // Handle primitive int conversions + if (to == int.class) { + if (IntConverter.compatibleTypes.contains(from)) { + return new IntConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Integer.class) { + // Handle boxed Integer conversions + if (IntConverter.compatibleTypes.contains(from)) { + return new BoxedIntConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == boolean.class) { + // Handle primitive boolean conversions + if (BooleanConverter.compatibleTypes.contains(from)) { + return new BooleanConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Boolean.class) { + // Handle boxed Boolean conversions + if (BooleanConverter.compatibleTypes.contains(from)) { + return new BoxedBooleanConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == byte.class) { + // Handle primitive byte conversions + if (ByteConverter.compatibleTypes.contains(from)) { + return new ByteConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Byte.class) { + // Handle boxed Byte conversions + if (ByteConverter.compatibleTypes.contains(from)) { + return new BoxedByteConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == short.class) { + // Handle primitive short conversions + if (ShortConverter.compatibleTypes.contains(from)) { + return new ShortConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Short.class) { + // Handle boxed Short conversions + if (ShortConverter.compatibleTypes.contains(from)) { + return new BoxedShortConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == long.class) { + // Handle primitive long conversions + if (LongConverter.compatibleTypes.contains(from)) { + return new LongConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Long.class) { + // Handle boxed Long conversions + if (LongConverter.compatibleTypes.contains(from)) { + return new BoxedLongConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == float.class) { + // Handle primitive float conversions + if (FloatConverter.compatibleTypes.contains(from)) { + return new FloatConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Float.class) { + // Handle boxed Float conversions + if (FloatConverter.compatibleTypes.contains(from)) { + return new BoxedFloatConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == double.class) { + // Handle primitive double conversions + if (DoubleConverter.compatibleTypes.contains(from)) { + return new DoubleConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == Double.class) { + // Handle boxed Double conversions + if (DoubleConverter.compatibleTypes.contains(from)) { + return new BoxedDoubleConverter(FieldAccessor.createAccessor(field)); + } + } else if (to == String.class) { + // Handle String conversions + if (StringConverter.compatibleTypes.contains(from)) { + return new StringConverter(FieldAccessor.createAccessor(field)); + } + } + + return null; // No compatible converter found + } + + /** + * Converter for primitive boolean fields. Converts compatible types to boolean values. Returns + * false for null values and incompatible types. + */ + public static class BooleanConverter extends FieldConverter { + static Set> compatibleTypes = ImmutableSet.of(String.class, Boolean.class); + + protected BooleanConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a boolean value. + * + * @param from the object to convert + * @return the converted boolean value + */ + public static Boolean convertFrom(Object from) { + if (from == null) { + return false; + } + if (from instanceof Boolean) { + return (Boolean) from; + } else if (from instanceof String) { + return Boolean.parseBoolean((String) from); + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Boolean convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Boolean fields. Converts compatible types to Boolean values. Returns null + * for null values, unlike the primitive version. + */ + public static class BoxedBooleanConverter extends FieldConverter { + protected BoxedBooleanConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a Boolean value. + * + * @param from the object to convert + * @return the converted Boolean value, or null if from is null + */ + public static Boolean convertFrom(Object from) { + if (from == null) { + return null; + } + return BooleanConverter.convertFrom(from); + } + + @Override + public Boolean convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for primitive byte fields. Converts compatible types to byte values. Returns 0 for + * null values. + */ + public static class ByteConverter extends FieldConverter { + static Set> compatibleTypes = + ImmutableSet.of(String.class, Integer.class, Long.class, Short.class, Byte.class); + + protected ByteConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a byte value. + * + * @param from the object to convert + * @return the converted byte value + * @throws NumberFormatException if the string cannot be parsed as a byte + * @throws ArithmeticException if the numeric value is out of byte range + */ + public static Byte convertFrom(Object from) { + if (from == null) { + return 0; + } + if (from instanceof Byte) { + return (Byte) from; + } else if (from instanceof Integer) { + return ((Integer) from).byteValue(); + } else if (from instanceof Long) { + return ((Long) from).byteValue(); + } else if (from instanceof Short) { + return ((Short) from).byteValue(); + } else if (from instanceof String) { + return Byte.parseByte((String) from); + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Byte convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Byte fields. Converts compatible types to Byte values. Returns null for + * null values, unlike the primitive version. + */ + public static class BoxedByteConverter extends FieldConverter { + protected BoxedByteConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a Byte value. + * + * @param from the object to convert + * @return the converted Byte value, or null if from is null + */ + public static Byte convertFrom(Object from) { + if (from == null) { + return null; + } + return ByteConverter.convertFrom(from); + } + + @Override + public Byte convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for primitive short fields. Converts compatible types to short values. Returns 0 for + * null values. + */ + public static class ShortConverter extends FieldConverter { + static Set> compatibleTypes = + ImmutableSet.of(String.class, Integer.class, Long.class, Short.class); + + protected ShortConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a short value. + * + * @param from the object to convert + * @return the converted short value + * @throws NumberFormatException if the string cannot be parsed as a short + * @throws ArithmeticException if the numeric value is out of short range + */ + public static Short convertFrom(Object from) { + if (from == null) { + return 0; + } + if (from instanceof Short) { + return (Short) from; + } else if (from instanceof Integer) { + return ((Integer) from).shortValue(); + } else if (from instanceof Long) { + return ((Long) from).shortValue(); + } else if (from instanceof String) { + return Short.parseShort((String) from); + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Short convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Short fields. Converts compatible types to Short values. Returns null for + * null values, unlike the primitive version. + */ + public static class BoxedShortConverter extends FieldConverter { + protected BoxedShortConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + public static Short convertFrom(Object from) { + if (from == null) { + return null; + } + return ShortConverter.convertFrom(from); + } + + @Override + public Short convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for primitive int fields. Converts compatible types to int values. Returns 0 for null + * values. + */ + public static class IntConverter extends FieldConverter { + static Set> compatibleTypes = ImmutableSet.of(String.class, Long.class, Integer.class); + + protected IntConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to an int value. + * + * @param from the object to convert + * @return the converted int value + * @throws NumberFormatException if the string cannot be parsed as an int + * @throws ArithmeticException if the numeric value is out of int range + */ + public static Integer convertFrom(Object from) { + if (from == null) { + return 0; + } + if (from instanceof Long) { + return Math.toIntExact((Long) from); + } else if (from instanceof Integer) { + return (Integer) from; + } else if (from instanceof String) { + return Integer.parseInt((String) from); + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Integer convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Integer fields. Converts compatible types to Integer values. Returns null + * for null values, unlike the primitive version. + */ + public static class BoxedIntConverter extends FieldConverter { + protected BoxedIntConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to an Integer value. + * + * @param from the object to convert + * @return the converted Integer value, or null if from is null + */ + public static Integer convertFrom(Object from) { + if (from == null) { + return null; + } + return IntConverter.convertFrom(from); + } + + @Override + public Integer convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for primitive long fields. Converts compatible types to long values. Returns 0 for + * null values. + */ + public static class LongConverter extends FieldConverter { + static Set> compatibleTypes = ImmutableSet.of(String.class, Long.class); + + protected LongConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a long value. + * + * @param from the object to convert + * @return the converted long value + * @throws NumberFormatException if the string cannot be parsed as a long + */ + public static Long convertFrom(Object from) { + if (from == null) { + return 0L; + } + if (from instanceof Long) { + return (Long) from; + } else if (from instanceof String) { + return Long.parseLong((String) from); + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Long convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Long fields. Converts compatible types to Long values. Returns null for + * null values, unlike the primitive version. + */ + public static class BoxedLongConverter extends FieldConverter { + protected BoxedLongConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a Long value. + * + * @param from the object to convert + * @return the converted Long value, or null if from is null + */ + public static Long convertFrom(Object from) { + if (from == null) { + return null; + } + return LongConverter.convertFrom(from); + } + + @Override + public Long convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for primitive float fields. Converts compatible types to float values. Returns 0.0f + * for null values. Only allows conversion from String. + */ + public static class FloatConverter extends FieldConverter { + static Set> compatibleTypes = ImmutableSet.of(String.class, Float.class); + + protected FloatConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a float value. + * + * @param from the object to convert + * @return the converted float value + * @throws NumberFormatException if the string cannot be parsed as a float + */ + public static Float convertFrom(Object from) { + if (from == null) { + return 0.0f; + } + if (from instanceof String) { + return Float.parseFloat((String) from); + } else if (from instanceof Float) { + return (Float) from; + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Float convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Float fields. Converts compatible types to Float values. Returns null for + * null values, unlike the primitive version. Only allows conversion from String. + */ + public static class BoxedFloatConverter extends FieldConverter { + protected BoxedFloatConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a Float value. + * + * @param from the object to convert + * @return the converted Float value, or null if from is null + */ + public static Float convertFrom(Object from) { + if (from == null) { + return null; + } + return FloatConverter.convertFrom(from); + } + + @Override + public Float convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for primitive double fields. Converts compatible types to double values. Returns 0.0 + * for null values. Allows conversion from String and Float. + */ + public static class DoubleConverter extends FieldConverter { + static Set> compatibleTypes = ImmutableSet.of(String.class, Float.class, Double.class); + + protected DoubleConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a double value. + * + * @param from the object to convert + * @return the converted double value + * @throws NumberFormatException if the string cannot be parsed as a double + */ + public static Double convertFrom(Object from) { + if (from == null) { + return 0.0; + } + if (from instanceof String) { + return Double.parseDouble((String) from); + } else if (from instanceof Double) { + return (Double) from; + } else if (from instanceof Float) { + return ((Float) from).doubleValue(); + } else { + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public Double convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for boxed Double fields. Converts compatible types to Double values. Returns null for + * null values, unlike the primitive version. Allows conversion from String and Float. + */ + public static class BoxedDoubleConverter extends FieldConverter { + protected BoxedDoubleConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a Double value. + * + * @param from the object to convert + * @return the converted Double value, or null if from is null + */ + public static Double convertFrom(Object from) { + if (from == null) { + return null; + } + return DoubleConverter.convertFrom(from); + } + + @Override + public Double convert(Object from) { + return convertFrom(from); + } + } + + /** + * Converter for String fields. Converts compatible types to String values. Only allows conversion + * from Number types to prevent malicious toString() calls. + */ + public static class StringConverter extends FieldConverter { + static Set> compatibleTypes = + ImmutableSet.of( + Integer.class, + Long.class, + Short.class, + Byte.class, + Boolean.class, + Float.class, + Double.class); + + protected StringConverter(FieldAccessor fieldAccessor) { + super(fieldAccessor); + } + + /** + * Converts an object to a String value. + * + * @param from the object to convert + * @return the converted String value, or null if from is null + */ + public static String convertFrom(Object from) { + if (from == null) { + return null; + } else if (from instanceof Number || from instanceof Boolean) { + return from.toString(); + } else { + // disallow on other types, to avoid malicious toString get called. + throw new UnsupportedOperationException("Incompatible type: " + from.getClass()); + } + } + + @Override + public String convert(Object from) { + return convertFrom(from); + } + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java b/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java index 06d09a5d5d..decb2b93a1 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java @@ -50,6 +50,7 @@ import org.apache.fory.collection.Tuple2; import org.apache.fory.memory.Platform; import org.apache.fory.reflect.TypeRef; +import org.apache.fory.serializer.converter.FieldConverter; import org.apache.fory.util.Preconditions; import org.apache.fory.util.StringUtils; import org.apache.fory.util.record.RecordComponent; @@ -89,6 +90,7 @@ public static void clearDescriptorCache() { private ForyField foryField; private boolean nullable; private boolean trackingRef; + private FieldConverter fieldConverter; public Descriptor(Field field, TypeRef typeRef, Method readMethod, Method writeMethod) { this.field = field; @@ -185,6 +187,7 @@ public Descriptor(DescriptorBuilder builder) { this.trackingRef = builder.trackingRef; this.type = builder.type; this.foryField = builder.foryField; + this.fieldConverter = builder.fieldConverter; } public DescriptorBuilder copyBuilder() { @@ -279,6 +282,14 @@ public TypeRef getTypeRef() { return typeRef; } + public FieldConverter getFieldConverter() { + return fieldConverter; + } + + public void setFieldConverter(FieldConverter fieldConverter) { + this.fieldConverter = fieldConverter; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder("Descriptor{"); diff --git a/java/fory-core/src/main/java/org/apache/fory/type/DescriptorBuilder.java b/java/fory-core/src/main/java/org/apache/fory/type/DescriptorBuilder.java index 643ab5a94c..6e75c49667 100644 --- a/java/fory-core/src/main/java/org/apache/fory/type/DescriptorBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/type/DescriptorBuilder.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import org.apache.fory.annotation.ForyField; import org.apache.fory.reflect.TypeRef; +import org.apache.fory.serializer.converter.FieldConverter; public class DescriptorBuilder { @@ -38,6 +39,7 @@ public class DescriptorBuilder { ForyField foryField; boolean nullable; boolean trackingRef; + FieldConverter fieldConverter; public DescriptorBuilder(Descriptor descriptor) { this.typeRef = descriptor.getTypeRef(); @@ -52,6 +54,7 @@ public DescriptorBuilder(Descriptor descriptor) { this.foryField = descriptor.getForyField(); this.nullable = descriptor.isNullable(); this.trackingRef = descriptor.isTrackingRef(); + this.fieldConverter = descriptor.getFieldConverter(); } public DescriptorBuilder typeRef(TypeRef typeRef) { diff --git a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties index 2e908e4191..8796e6db05 100644 --- a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties +++ b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties @@ -439,6 +439,18 @@ Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\ org.apache.fory.serializer.LazySerializer$LazyObjectSerializer,\ org.apache.fory.serializer.shim.ShimDispatcher,\ org.apache.fory.serializer.shim.ProtobufDispatcher,\ + org.apache.fory.serializer.converter.FieldConverter,\ + org.apache.fory.serializer.converter.FieldConverters$BooleanConverter,\ + org.apache.fory.serializer.converter.FieldConverters$BoxedBooleanConverter,\ + org.apache.fory.serializer.converter.FieldConverters$ByteConverter,\ + org.apache.fory.serializer.converter.FieldConverters$BoxedByteConverter,\ + org.apache.fory.serializer.converter.FieldConverters$ShortConverter,\ + org.apache.fory.serializer.converter.FieldConverters$BoxedShortConverter,\ + org.apache.fory.serializer.converter.FieldConverters$IntConverter,\ + org.apache.fory.serializer.converter.FieldConverters$BoxedIntConverter,\ + org.apache.fory.serializer.converter.FieldConverters$LongConverter,\ + org.apache.fory.serializer.converter.FieldConverters$BoxedLongConverter,\ + org.apache.fory.serializer.converter.FieldConverters$StringConverter,\ org.apache.fory.serializer.ObjectStreamSerializer,\ org.apache.fory.serializer.ObjectStreamSerializer$1,\ org.apache.fory.serializer.ObjectStreamSerializer$StreamClassInfo,\ diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/MetaSharedCompatibleTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/MetaSharedCompatibleTest.java index 9df8ca47b4..eecb7aa7d0 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/MetaSharedCompatibleTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/MetaSharedCompatibleTest.java @@ -41,6 +41,8 @@ import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.MetaContext; import org.apache.fory.serializer.collection.UnmodifiableSerializersTest; +import org.apache.fory.serializer.converter.FieldConverter; +import org.apache.fory.serializer.converter.FieldConverters; import org.apache.fory.test.bean.BeanA; import org.apache.fory.test.bean.BeanB; import org.apache.fory.test.bean.CollectionFields; @@ -939,4 +941,147 @@ public void testInheritance() throws Exception { Assert.assertEquals(ReflectionUtils.getObjectFieldValue(o1, "f2"), 20); Assert.assertEquals(ReflectionUtils.getObjectFieldValue(o1, "f3"), 30); } + + @Test + public void testCompatibleFieldConvert() throws Exception { + byte[] bytes; + Object o1; + ImmutableSet floatFields = ImmutableSet.of("f11", "f12", "f13", "f14"); + { + CompileUnit compileUnit = + new CompileUnit( + "", + "CompatibleFieldConvert", + ("public final class CompatibleFieldConvert {\n" + + " public boolean ftrue;\n" + + " public Boolean ffalse;\n" + + " public byte f3;\n" + + " public Byte f4;\n" + + " public short f5;\n" + + " public Short f6;\n" + + " public int f7;\n" + + " public Integer f8;\n" + + " public long f9;\n" + + " public Long f10;\n" + + " public float f11;\n" + + " public Float f12;\n" + + " public double f13;\n" + + " public Double f14;\n" + + " public String toString() {return \"\" + ftrue + ffalse + " + + "f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + f11 + f12 + f13 + f14;}\n" + + "}")); + + ClassLoader classLoader = + JaninoUtils.compile(Thread.currentThread().getContextClassLoader(), compileUnit); + Class cls = classLoader.loadClass(compileUnit.getQualifiedClassName()); + o1 = cls.newInstance(); + for (Field field : ReflectionUtils.getSortedFields(cls, false)) { + String name = field.getName(); + field.setAccessible(true); + FieldConverter converter = FieldConverters.getConverter(String.class, field); + Assert.assertNotNull(converter); + Object converted = converter.convert(name.substring(1)); + field.set(o1, converted); + } + Fory fory = + builder() + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withClassLoader(classLoader) + .build(); + bytes = fory.serialize(o1); + } + { + CompileUnit compileUnit = + new CompileUnit( + "", + "CompatibleFieldConvert", + ("public final class CompatibleFieldConvert {\n" + + " public Boolean ftrue;\n" + + " public boolean ffalse;\n" + + " public Byte f3;\n" + + " public byte f4;\n" + + " public Short f5;\n" + + " public short f6;\n" + + " public Integer f7;\n" + + " public int f8;\n" + + " public Long f9;\n" + + " public long f10;\n" + + " public Float f11;\n" + + " public float f12;\n" + + " public Double f13;\n" + + " public double f14;\n" + + " public String toString() {return \"\" + ftrue + ffalse + " + + "f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + f11 + f12 + f13 + f14;}\n" + + "}")); + ClassLoader classLoader = + JaninoUtils.compile(Thread.currentThread().getContextClassLoader(), compileUnit); + Class cls = classLoader.loadClass(compileUnit.getQualifiedClassName()); + Assert.assertNotEquals(cls, o1.getClass()); + Fory fory = + builder() + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withClassLoader(classLoader) + .build(); + Object o = fory.deserialize(bytes); + Assert.assertEquals(o.getClass(), cls); + List fields = ReflectionUtils.getSortedFields(cls, false); + for (Field field : fields) { + field.setAccessible(true); + Object fieldValue = field.get(o); + if (fieldValue instanceof Float || fieldValue instanceof Double) { + Assert.assertEquals(fieldValue.toString(), field.getName().substring(1) + ".0"); + } else { + Assert.assertEquals(fieldValue.toString(), field.getName().substring(1)); + } + } + Assert.assertEquals(o.toString(), o1.toString()); + } + { + CompileUnit compileUnit = + new CompileUnit( + "", + "CompatibleFieldConvert", + ("public final class CompatibleFieldConvert {\n" + + " public String ftrue;\n" + + " public String ffalse;\n" + + " public String f3;\n" + + " public String f4;\n" + + " public String f5;\n" + + " public String f6;\n" + + " public String f7;\n" + + " public String f8;\n" + + " public String f9;\n" + + " public String f10;\n" + + " public String f11;\n" + + " public String f12;\n" + + " public String f13;\n" + + " public String f14;\n" + + " public String toString() {return \"\" + ftrue + ffalse + " + + "f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + f11 + f12 + f13 + f14;}\n" + + "}")); + + ClassLoader classLoader = + JaninoUtils.compile(Thread.currentThread().getContextClassLoader(), compileUnit); + Fory fory = + builder() + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withClassLoader(classLoader) + .build(); + Class cls = classLoader.loadClass(compileUnit.getQualifiedClassName()); + Assert.assertNotEquals(cls, o1.getClass()); + Object o = fory.deserialize(bytes); + Assert.assertEquals(o.getClass(), cls); + List fields = ReflectionUtils.getSortedFields(cls, false); + for (Field field : fields) { + field.setAccessible(true); + Object fieldValue = field.get(o); + if (floatFields.contains(field.getName())) { + Assert.assertEquals(fieldValue.toString(), field.getName().substring(1) + ".0"); + } else { + Assert.assertEquals(fieldValue.toString(), field.getName().substring(1)); + } + } + Assert.assertEquals(o.toString(), o1.toString()); + } + } }