This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 8b56bc53f fix(java): preserve collection TypeDef serializer family 
(#3827)
8b56bc53f is described below

commit 8b56bc53f758ee4a5a33ab78e764887fcac185ce
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jul 8 22:33:46 2026 +0530

    fix(java): preserve collection TypeDef serializer family (#3827)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    Closes #3826
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, on the current PR diff or current HEAD after the latest code
    changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 .../org/apache/fory/resolver/ClassResolver.java    |  17 +-
 .../org/apache/fory/resolver/TypeResolver.java     |  31 +-
 .../serializer/CompatibleLayerSerializerBase.java  |  13 +
 .../collection/ChildContainerSerializers.java      |  73 ++--
 .../collection/CollectionSerializers.java          |  30 +-
 .../fory/serializer/collection/MapSerializers.java |  29 +-
 .../CollectionCompatibleSerializerTest.java        | 377 +++++++++++++++++++++
 7 files changed, 535 insertions(+), 35 deletions(-)

diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 7b12fae6b..e5a144707 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -954,7 +954,13 @@ public class ClassResolver extends TypeResolver {
     if (hasFieldMetadata) {
       // Preserve the normal TypeInfo/name cache so locally generated or 
dynamically registered
       // classes can be resolved when the TypeDef is decoded by the same 
resolver.
-      getTypeIdForTypeDef(cls);
+      int typeId = getTypeIdForTypeDef(cls);
+      if (usesNonStructTypeDef(cls)) {
+        // Field metadata does not make collection/map/custom-serializer 
classes struct-owned.
+        // Their natural serializers may delegate field IO internally, but the 
TypeDef root kind
+        // must still resolve to the non-struct serializer family on the 
reader.
+        return normalizeTypeDefRootTypeId(cls, typeId);
+      }
       return getFieldMetadataTypeIdForTypeDef(cls);
     }
     TypeInfo typeInfo = classInfoMap.get(cls);
@@ -2011,7 +2017,9 @@ public class ClassResolver extends TypeResolver {
     TypeDef typeDef;
     Preconditions.checkArgument(
         serializerClass != 
UnknownClassSerializers.UnknownStructSerializer.class);
-    if (needToWriteTypeDef(serializerClass)) {
+    if (needToWriteTypeDef(serializerClass) || 
needsCollectionFieldTypeDef(serializerClass)) {
+      // Default collection/map serializers remain non-struct roots, but their 
wrapper fields still
+      // need TypeDef metadata so remote compatible readers can evolve those 
fields.
       typeDef = typeDefMap.computeIfAbsent(typeInfo.type, cls -> 
TypeDef.buildTypeDef(this, cls));
     } else {
       // Some type will use other serializers such MapSerializer and so on.
@@ -2026,6 +2034,11 @@ public class ClassResolver extends TypeResolver {
     return typeDef;
   }
 
+  private static boolean needsCollectionFieldTypeDef(Class<? extends 
Serializer> serializerClass) {
+    return serializerClass == 
CollectionSerializers.DefaultJavaCollectionSerializer.class
+        || serializerClass == MapSerializers.DefaultJavaMapSerializer.class;
+  }
+
   /**
    * Write classname for java serialization. Note that the object of provided 
class can be
    * non-serializable, and class with writeReplace/readResolve defined won't 
be skipped. For
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index fde985f4e..a78a38f73 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -93,6 +93,8 @@ import org.apache.fory.serializer.UnknownClass;
 import org.apache.fory.serializer.UnknownClass.UnknownEmptyStruct;
 import org.apache.fory.serializer.UnknownClass.UnknownStruct;
 import org.apache.fory.serializer.UnknownClassSerializers;
+import org.apache.fory.serializer.collection.CollectionSerializers;
+import org.apache.fory.serializer.collection.MapSerializers;
 import org.apache.fory.type.Descriptor;
 import org.apache.fory.type.DescriptorBuilder;
 import org.apache.fory.type.DescriptorGrouper;
@@ -1136,7 +1138,10 @@ public abstract class TypeResolver {
     TypeInfo typeInfo;
     if (!typeDef.isStructSchemaKind()
         && 
!UnknownClass.class.isAssignableFrom(TypeUtils.getComponentIfArray(cls))) {
-      typeInfo = getTypeInfo(cls);
+      typeInfo =
+          typeDef.getFieldsInfo().isEmpty()
+              ? getTypeInfo(cls)
+              : getMetaSharedTypeInfo(typeDef, cls);
     } else if (ClassResolver.useReplaceResolveSerializer(cls)) {
       // For classes with writeReplace/readResolve, use their natural 
serializer
       // (ReplaceResolveSerializer) instead of CompatibleSerializer
@@ -1194,6 +1199,14 @@ public abstract class TypeResolver {
           || (Types.isUnionType(streamTypeId) && 
!Types.isUnionType(localTypeInfo.typeId))) {
         throw new SerializerUnregisteredException(typeDef.getClassName());
       }
+      Serializer<?> serializer = newMetaSharedExtSerializer(cls, typeDef, 
localTypeInfo);
+      if (serializer != null) {
+        TypeInfo typeInfo =
+            new TypeInfo(this, cls, null, localTypeInfo.typeId, 
typeDef.getClassSpec().userTypeId);
+        typeInfo.typeDef = typeDef;
+        typeInfo.setSerializer(this, serializer);
+        return typeInfo;
+      }
       return localTypeInfo;
     }
     Integer classId = extRegistry.registeredClassIdMap.get(cls);
@@ -1301,6 +1314,22 @@ public abstract class TypeResolver {
     return typeInfo;
   }
 
+  private Serializer<?> newMetaSharedExtSerializer(
+      Class<?> cls, TypeDef typeDef, TypeInfo localTypeInfo) {
+    if (typeDef.getFieldsInfo().isEmpty()) {
+      return null;
+    }
+    Serializer<?> serializer = localTypeInfo.getSerializer();
+    Class<?> serializerClass = serializer.getClass();
+    if (serializerClass == 
CollectionSerializers.DefaultJavaCollectionSerializer.class) {
+      return new CollectionSerializers.DefaultJavaCollectionSerializer<>(this, 
cls, typeDef);
+    }
+    if (serializerClass == MapSerializers.DefaultJavaMapSerializer.class) {
+      return new MapSerializers.DefaultJavaMapSerializer<>(this, cls, typeDef);
+    }
+    return null;
+  }
+
   private Serializer<?> newGeneratedCompatibleSerializer(
       Class<?> cls, Class<? extends Serializer> serializerClass, TypeDef 
typeDef) {
     try {
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
index 67fab5f10..3ba12f6a5 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.serializer;
 
+import org.apache.fory.annotation.Internal;
 import org.apache.fory.collection.IdentityObjectIntMap;
 import org.apache.fory.collection.ObjectIntMap;
 import org.apache.fory.context.CopyContext;
@@ -156,6 +157,18 @@ public abstract class CompatibleLayerSerializerBase<T> 
extends AbstractObjectSer
     return allFields.length;
   }
 
+  @Internal
+  public final TypeDef getLayerTypeDef() {
+    checkLayerSerializerMeta();
+    return layerTypeDef;
+  }
+
+  @Internal
+  public final Class<?> getLayerMarkerClass() {
+    checkLayerSerializerMeta();
+    return layerMarkerClass;
+  }
+
   @SuppressWarnings("rawtypes")
   public void populateFieldInfo(ObjectIntMap fieldIndexMap, Class<?>[] 
fieldTypes) {
     checkLayerSerializerMeta();
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
index 49c65b56c..a307d496c 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
@@ -25,6 +25,7 @@ import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Field;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -54,9 +55,11 @@ import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.meta.TypeDef;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.resolver.ClassResolver;
+import org.apache.fory.resolver.TypeInfo;
 import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.serializer.AbstractObjectSerializer;
 import org.apache.fory.serializer.CompatibleLayerSerializer;
+import org.apache.fory.serializer.CompatibleLayerSerializerBase;
 import org.apache.fory.serializer.FieldGroups;
 import org.apache.fory.serializer.FieldGroups.SerializationFieldInfo;
 import org.apache.fory.serializer.JavaSerializer;
@@ -645,12 +648,11 @@ public class ChildContainerSerializers {
     readAndCheckNumClassLayers(readContext, collection.getClass(), 
slotsSerializers.length);
     for (Serializer slotsSerializer : slotsSerializers) {
       if (slotsSerializer instanceof CompatibleLayerSerializer) {
-        CompatibleLayerSerializer compatibleSerializer =
-            (CompatibleLayerSerializer) slotsSerializer;
-        // Read layer class meta first if meta share is enabled
-        // This corresponds to writeLayerClassMeta() in 
CompatibleLayerSerializer.write()
+        CompatibleLayerSerializerBase compatibleSerializer =
+            (CompatibleLayerSerializerBase) slotsSerializer;
         if (typeResolver.getConfig().isMetaShareEnabled()) {
-          readAndSkipLayerClassMeta(readContext);
+          compatibleSerializer =
+              readLayerSerializer(readContext, typeResolver, 
compatibleSerializer);
         }
         compatibleSerializer.readAndSetFields(readContext, collection);
       } else {
@@ -686,16 +688,16 @@ public class ChildContainerSerializers {
   }
 
   /**
-   * Read and skip the layer class meta from buffer. This is used to skip over 
the class definition
-   * that was written by CompatibleLayerSerializer.writeLayerClassMeta(). For
-   * ChildContainerSerializers, we use the same serializer on both write and 
read sides, so we just
-   * need to skip the meta without actually parsing it.
+   * Read the layer class meta that was written by 
CompatibleLayerSerializer.writeLayerClassMeta().
    */
-  private static void readAndSkipLayerClassMeta(ReadContext readContext) {
+  private static CompatibleLayerSerializerBase readLayerSerializer(
+      ReadContext readContext,
+      TypeResolver typeResolver,
+      CompatibleLayerSerializerBase localSerializer) {
     MemoryBuffer buffer = readContext.getBuffer();
     MetaReadContext metaReadContext = readContext.getMetaReadContext();
     if (metaReadContext == null) {
-      return;
+      return localSerializer;
     }
     int indexMarker = buffer.readVarUInt32Small14();
     boolean isRef = (indexMarker & 1) == 1;
@@ -704,16 +706,47 @@ public class ChildContainerSerializers {
       if (index >= metaReadContext.readTypeInfos.size) {
         throw new ForyException("Invalid layer metadata reference id " + 
index);
       }
-      // Reference to previously read type - nothing more to read
-      return;
+      TypeInfo typeInfo = metaReadContext.readTypeInfos.get(index);
+      if (typeInfo == null) {
+        throw new ForyException("Invalid layer metadata reference id " + 
index);
+      }
+      return getLayerSerializer(typeResolver, localSerializer, typeInfo);
     }
-    // New type - need to read and skip the TypeDef bytes
     long id = buffer.readInt64();
-    TypeDef.skipTypeDef(buffer, id);
-    // Add a placeholder to keep readTypeInfos indices in sync with the write 
side's classMap.
-    // The write side (writeLayerClassMeta) adds layer marker classes to 
classMap which shares
-    // the same index space as writeSharedClassMeta. Without this placeholder, 
subsequent
-    // readSharedClassMeta reference lookups would use wrong indices.
-    metaReadContext.readTypeInfos.add(null);
+    TypeInfo typeInfo = readLayerTypeInfo(typeResolver, buffer, 
localSerializer, id);
+    metaReadContext.readTypeInfos.add(typeInfo);
+    return getLayerSerializer(typeResolver, localSerializer, typeInfo);
+  }
+
+  private static TypeInfo readLayerTypeInfo(
+      TypeResolver typeResolver,
+      MemoryBuffer buffer,
+      CompatibleLayerSerializerBase localSerializer,
+      long typeDefId) {
+    TypeDef localTypeDef = localSerializer.getLayerTypeDef();
+    byte[] encoded = TypeDef.readTypeDefBytes(typeResolver, buffer, typeDefId);
+    Class<?> layerClass = localSerializer.getType();
+    typeResolver.checkClassForDeserialization(layerClass);
+    TypeDef typeDef =
+        Arrays.equals(encoded, localTypeDef.getEncoded())
+            ? localTypeDef
+            : 
typeResolver.cacheRemoteTypeDef(TypeDef.readTypeDef(typeResolver, encoded));
+    return new TypeInfo(layerClass, typeDef);
+  }
+
+  private static CompatibleLayerSerializerBase getLayerSerializer(
+      TypeResolver typeResolver, CompatibleLayerSerializerBase 
localSerializer, TypeInfo typeInfo) {
+    Serializer<?> serializer = typeInfo.getSerializer();
+    if (serializer != null) {
+      return (CompatibleLayerSerializerBase) serializer;
+    }
+    CompatibleLayerSerializer<?> newSerializer =
+        new CompatibleLayerSerializer(
+            typeResolver,
+            localSerializer.getType(),
+            typeInfo.getTypeDef(),
+            localSerializer.getLayerMarkerClass());
+    typeInfo.setSerializer(newSerializer);
+    return newSerializer;
   }
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
index 5dc330494..81c2355bb 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
@@ -56,12 +56,14 @@ import org.apache.fory.exception.DeserializationException;
 import org.apache.fory.exception.ForyException;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
+import org.apache.fory.meta.TypeDef;
 import org.apache.fory.reflect.FieldAccessor;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.resolver.ClassResolver;
 import org.apache.fory.resolver.TypeInfo;
 import org.apache.fory.resolver.TypeInfoHolder;
 import org.apache.fory.resolver.TypeResolver;
+import org.apache.fory.serializer.CompatibleSerializer;
 import org.apache.fory.serializer.ExternalizableSerializer;
 import org.apache.fory.serializer.GraphMemoryEstimates;
 import org.apache.fory.serializer.ReplaceResolveSerializer;
@@ -1050,17 +1052,33 @@ public class CollectionSerializers {
     private Serializer<T> dataSerializer;
 
     public DefaultJavaCollectionSerializer(TypeResolver typeResolver, Class<T> 
cls) {
+      this(typeResolver, cls, null, true);
+    }
+
+    public DefaultJavaCollectionSerializer(
+        TypeResolver typeResolver, Class<T> cls, TypeDef typeDef) {
+      this(typeResolver, cls, typeDef, false);
+    }
+
+    private DefaultJavaCollectionSerializer(
+        TypeResolver typeResolver, Class<T> cls, TypeDef typeDef, boolean 
registerSerializer) {
       super(typeResolver, cls, false);
       Preconditions.checkArgument(
           !config.isXlang(),
           "Fory cross-language default collection serializer should use "
               + CollectionSerializer.class);
-      typeResolver.setSerializer(cls, this);
-      Class<? extends Serializer> serializerClass =
-          ((ClassResolver) typeResolver)
-              .getObjectSerializerClass(
-                  cls, sc -> dataSerializer = 
Serializers.newSerializer(typeResolver, cls, sc));
-      dataSerializer = Serializers.newSerializer(typeResolver, cls, 
serializerClass);
+      if (registerSerializer) {
+        typeResolver.setSerializer(cls, this);
+      }
+      if (typeDef == null) {
+        Class<? extends Serializer> serializerClass =
+            ((ClassResolver) typeResolver)
+                .getObjectSerializerClass(
+                    cls, sc -> dataSerializer = 
Serializers.newSerializer(typeResolver, cls, sc));
+        dataSerializer = Serializers.newSerializer(typeResolver, cls, 
serializerClass);
+      } else {
+        dataSerializer = new CompatibleSerializer<>(typeResolver, cls, 
typeDef);
+      }
       // No need to set object serializer to this, it will be set in class 
resolver later.
       // typeResolver.setSerializer(cls, this);
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
index afc4c3647..2e402e061 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
@@ -49,10 +49,12 @@ import org.apache.fory.context.CopyContext;
 import org.apache.fory.context.ReadContext;
 import org.apache.fory.context.WriteContext;
 import org.apache.fory.memory.MemoryBuffer;
+import org.apache.fory.meta.TypeDef;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.resolver.ClassResolver;
 import org.apache.fory.resolver.TypeInfo;
 import org.apache.fory.resolver.TypeResolver;
+import org.apache.fory.serializer.CompatibleSerializer;
 import org.apache.fory.serializer.ExternalizableSerializer;
 import org.apache.fory.serializer.GraphMemoryEstimates;
 import org.apache.fory.serializer.ReplaceResolveSerializer;
@@ -510,16 +512,31 @@ public class MapSerializers {
     private Serializer<T> dataSerializer;
 
     public DefaultJavaMapSerializer(TypeResolver typeResolver, Class<T> cls) {
+      this(typeResolver, cls, null, true);
+    }
+
+    public DefaultJavaMapSerializer(TypeResolver typeResolver, Class<T> cls, 
TypeDef typeDef) {
+      this(typeResolver, cls, typeDef, false);
+    }
+
+    private DefaultJavaMapSerializer(
+        TypeResolver typeResolver, Class<T> cls, TypeDef typeDef, boolean 
registerSerializer) {
       super(typeResolver, cls, false);
       Preconditions.checkArgument(
           !config.isXlang(),
           "Fory cross-language default map serializer should use " + 
MapSerializer.class);
-      typeResolver.setSerializer(cls, this);
-      Class<? extends Serializer> serializerClass =
-          ((ClassResolver) typeResolver)
-              .getObjectSerializerClass(
-                  cls, sc -> dataSerializer = 
Serializers.newSerializer(typeResolver, cls, sc));
-      dataSerializer = Serializers.newSerializer(typeResolver, cls, 
serializerClass);
+      if (registerSerializer) {
+        typeResolver.setSerializer(cls, this);
+      }
+      if (typeDef == null) {
+        Class<? extends Serializer> serializerClass =
+            ((ClassResolver) typeResolver)
+                .getObjectSerializerClass(
+                    cls, sc -> dataSerializer = 
Serializers.newSerializer(typeResolver, cls, sc));
+        dataSerializer = Serializers.newSerializer(typeResolver, cls, 
serializerClass);
+      } else {
+        dataSerializer = new CompatibleSerializer<>(typeResolver, cls, 
typeDef);
+      }
       // No need to set object serializer to this, it will be set in class 
resolver later.
       // typeResolver.setSerializer(cls, this);
     }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionCompatibleSerializerTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionCompatibleSerializerTest.java
new file mode 100644
index 000000000..930727888
--- /dev/null
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionCompatibleSerializerTest.java
@@ -0,0 +1,377 @@
+/*
+ * 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.collection;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Field;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.tools.JavaCompiler;
+import javax.tools.ToolProvider;
+import org.apache.fory.Fory;
+import org.apache.fory.ForyTestBase;
+import org.apache.fory.context.MetaReadContext;
+import org.apache.fory.context.MetaWriteContext;
+import org.apache.fory.util.ClassLoaderUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class CollectionCompatibleSerializerTest extends ForyTestBase {
+  private static final String PEER_PACKAGE = 
"org.apache.fory.serializer.collection.compat";
+
+  @Test
+  public void testCustomSetCompatible() {
+    Fory writer = compatibleFory();
+    Fory reader = compatibleFory();
+    CustomSet set = new CustomSet("set-marker");
+    set.add("a");
+    set.add("b");
+
+    SetHolder holder = new SetHolder();
+    holder.values = set;
+
+    SetHolder copy = serDe(writer, reader, holder);
+    Assert.assertEquals(copy.values.getClass(), CustomSet.class);
+    CustomSet copySet = (CustomSet) copy.values;
+    Assert.assertEquals(copySet.getMarker(), "set-marker");
+    Assert.assertEquals(copySet, set);
+  }
+
+  @Test
+  public void testCustomMapCompatible() {
+    Fory writer = compatibleFory();
+    Fory reader = compatibleFory();
+    CustomMap map = new CustomMap("map-marker");
+    map.put("k1", "v1");
+    map.put("k2", "v2");
+
+    MapHolder holder = new MapHolder();
+    holder.values = map;
+
+    MapHolder copy = serDe(writer, reader, holder);
+    Assert.assertEquals(copy.values.getClass(), CustomMap.class);
+    CustomMap copyMap = (CustomMap) copy.values;
+    Assert.assertEquals(copyMap.getMarker(), "map-marker");
+    Assert.assertEquals(copyMap, map);
+  }
+
+  @Test
+  public void testDefaultSetEvolution() throws Exception {
+    Class<?> writerPeer = compilePeer("DefaultSetPeer", 
defaultSetSource(true));
+    Class<?> readerPeer = compilePeer("DefaultSetPeer", 
defaultSetSource(false));
+    Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+    Object copy = peerSerDe(writerPeer.getClassLoader(), 
readerPeer.getClassLoader(), writerHolder);
+    Object values = fieldValue(copy, "values");
+    Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE + 
".DefaultSetPeer$CustomSet");
+    Assert.assertEquals(fieldValue(values, "marker"), "set-marker");
+    Assert.assertEquals(((Collection<?>) values).size(), 2);
+    Assert.assertTrue(((Collection<?>) values).contains("a"));
+  }
+
+  @Test
+  public void testDefaultMapEvolution() throws Exception {
+    Class<?> writerPeer = compilePeer("DefaultMapPeer", 
defaultMapSource(true));
+    Class<?> readerPeer = compilePeer("DefaultMapPeer", 
defaultMapSource(false));
+    Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+    Object copy = peerSerDe(writerPeer.getClassLoader(), 
readerPeer.getClassLoader(), writerHolder);
+    Object values = fieldValue(copy, "values");
+    Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE + 
".DefaultMapPeer$CustomMap");
+    Assert.assertEquals(fieldValue(values, "marker"), "map-marker");
+    Assert.assertEquals(((Map<?, ?>) values).get("k1"), "v1");
+    Assert.assertEquals(((Map<?, ?>) values).size(), 2);
+  }
+
+  @Test
+  public void testChildSetEvolution() throws Exception {
+    Class<?> writerPeer = compilePeer("ChildSetPeer", childSetSource(true));
+    Class<?> readerPeer = compilePeer("ChildSetPeer", childSetSource(false));
+    Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+    Object copy = peerSerDe(writerPeer.getClassLoader(), 
readerPeer.getClassLoader(), writerHolder);
+    Object values = fieldValue(copy, "values");
+    Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE + 
".ChildSetPeer$CustomSet");
+    Assert.assertEquals(fieldValue(values, "marker"), "child-set-marker");
+    Assert.assertTrue(((Collection<?>) values).contains("a"));
+  }
+
+  @Test
+  public void testChildMapEvolution() throws Exception {
+    Class<?> writerPeer = compilePeer("ChildMapPeer", childMapSource(true));
+    Class<?> readerPeer = compilePeer("ChildMapPeer", childMapSource(false));
+    Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+    Object copy = peerSerDe(writerPeer.getClassLoader(), 
readerPeer.getClassLoader(), writerHolder);
+    Object values = fieldValue(copy, "values");
+    Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE + 
".ChildMapPeer$CustomMap");
+    Assert.assertEquals(fieldValue(values, "marker"), "child-map-marker");
+    Assert.assertEquals(((Map<?, ?>) values).get("k1"), "v1");
+  }
+
+  @Test
+  public void testObjectStreamEvolution() throws Exception {
+    Class<?> writerPeer = compilePeer("StreamPeer", streamSource(true));
+    Class<?> readerPeer = compilePeer("StreamPeer", streamSource(false));
+    Object writerValue = writerPeer.getMethod("newValue").invoke(null);
+
+    Object copy = peerSerDe(writerPeer.getClassLoader(), 
readerPeer.getClassLoader(), writerValue);
+    Assert.assertEquals(copy.getClass().getName(), PEER_PACKAGE + 
".StreamPeer");
+    Assert.assertEquals(fieldValue(copy, "marker"), "stream-marker");
+  }
+
+  private static Fory compatibleFory() {
+    return 
builder().withRefTracking(true).withCodegen(true).withCompatible(true).build();
+  }
+
+  private static Fory peerFory(ClassLoader classLoader) {
+    return builder()
+        .withClassLoader(classLoader)
+        .withRefTracking(true)
+        .withCodegen(true)
+        .withCompatible(true)
+        .withMetaShare(true)
+        .withScopedMetaShare(false)
+        .build();
+  }
+
+  private static Object peerSerDe(
+      ClassLoader writerClassLoader, ClassLoader readerClassLoader, Object 
value) {
+    Fory writer = peerFory(writerClassLoader);
+    Fory reader = peerFory(readerClassLoader);
+    MetaWriteContext metaWriteContext = new MetaWriteContext();
+    MetaReadContext metaReadContext = new MetaReadContext();
+    setMetaContexts(writer, metaWriteContext, new MetaReadContext());
+    byte[] bytes = writer.serialize(value);
+    setMetaContexts(reader, new MetaWriteContext(), metaReadContext);
+    return reader.deserialize(bytes);
+  }
+
+  private static Object fieldValue(Object obj, String name) throws 
ReflectiveOperationException {
+    Field field = obj.getClass().getField(name);
+    return field.get(obj);
+  }
+
+  private static Class<?> compilePeer(String className, String source) throws 
Exception {
+    Path dir = Files.createTempDirectory("fory-compatible-collection");
+    Path sourceFile = dir.resolve(PEER_PACKAGE.replace('.', 
'/')).resolve(className + ".java");
+    Files.createDirectories(sourceFile.getParent());
+    Files.write(sourceFile, source.getBytes(StandardCharsets.UTF_8));
+    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+    Assert.assertNotNull(compiler, "Tests require a JDK compiler");
+    int result =
+        compiler.run(
+            null,
+            new ByteArrayOutputStream(),
+            System.err,
+            "-classpath",
+            System.getProperty("java.class.path"),
+            "-d",
+            dir.toString(),
+            sourceFile.toString());
+    if (result != 0) {
+      throw new AssertionError("Couldn't compile code:\n" + source);
+    }
+    URL[] urls = new URL[] {dir.toUri().toURL()};
+    ClassLoader parent = 
CollectionCompatibleSerializerTest.class.getClassLoader();
+    return new ClassLoaderUtils.ChildFirstURLClassLoader(urls, parent)
+        .loadClass(PEER_PACKAGE + "." + className);
+  }
+
+  private static String defaultSetSource(boolean writer) {
+    return sourceHeader("DefaultSetPeer")
+        + "  public static class Holder { public Set values; }\n"
+        + "  public static class CustomSet extends AbstractSet {\n"
+        + "    public LinkedHashSet values = new LinkedHashSet();\n"
+        + "    public String marker;\n"
+        + (writer ? "    public String writerOnly;\n" : "")
+        + "    public Iterator iterator() { return values.iterator(); }\n"
+        + "    public int size() { return values.size(); }\n"
+        + "    public boolean add(Object value) { return values.add(value); 
}\n"
+        + "  }\n"
+        + "  public static Holder newHolder() {\n"
+        + "    Holder holder = new Holder();\n"
+        + "    CustomSet values = new CustomSet();\n"
+        + "    values.marker = \"set-marker\";\n"
+        + (writer ? "    values.writerOnly = \"writer-only\";\n" : "")
+        + "    values.add(\"a\"); values.add(\"b\"); holder.values = values; 
return holder;\n"
+        + "  }\n"
+        + "}\n";
+  }
+
+  private static String defaultMapSource(boolean writer) {
+    return sourceHeader("DefaultMapPeer")
+        + "  public static class Holder { public Map values; }\n"
+        + "  public static class CustomMap extends AbstractMap {\n"
+        + "    public LinkedHashMap values = new LinkedHashMap();\n"
+        + "    public String marker;\n"
+        + (writer ? "    public String writerOnly;\n" : "")
+        + "    public Object put(Object key, Object value) { return 
values.put(key, value); }\n"
+        + "    public Set entrySet() { return values.entrySet(); }\n"
+        + "  }\n"
+        + "  public static Holder newHolder() {\n"
+        + "    Holder holder = new Holder();\n"
+        + "    CustomMap values = new CustomMap();\n"
+        + "    values.marker = \"map-marker\";\n"
+        + (writer ? "    values.writerOnly = \"writer-only\";\n" : "")
+        + "    values.put(\"k1\", \"v1\"); values.put(\"k2\", \"v2\");\n"
+        + "    holder.values = values; return holder;\n"
+        + "  }\n"
+        + "}\n";
+  }
+
+  private static String childSetSource(boolean writer) {
+    return sourceHeader("ChildSetPeer")
+        + "  public static class Holder { public Set values; }\n"
+        + "  public static class CustomSet extends HashSet {\n"
+        + "    public String marker;\n"
+        + (writer ? "    public String writerOnly;\n" : "")
+        + "  }\n"
+        + "  public static Holder newHolder() {\n"
+        + "    Holder holder = new Holder();\n"
+        + "    CustomSet values = new CustomSet();\n"
+        + "    values.marker = \"child-set-marker\";\n"
+        + (writer ? "    values.writerOnly = \"writer-only\";\n" : "")
+        + "    values.add(\"a\"); values.add(\"b\"); holder.values = values; 
return holder;\n"
+        + "  }\n"
+        + "}\n";
+  }
+
+  private static String childMapSource(boolean writer) {
+    return sourceHeader("ChildMapPeer")
+        + "  public static class Holder { public Map values; }\n"
+        + "  public static class CustomMap extends HashMap {\n"
+        + "    public String marker;\n"
+        + (writer ? "    public String writerOnly;\n" : "")
+        + "  }\n"
+        + "  public static Holder newHolder() {\n"
+        + "    Holder holder = new Holder();\n"
+        + "    CustomMap values = new CustomMap();\n"
+        + "    values.marker = \"child-map-marker\";\n"
+        + (writer ? "    values.writerOnly = \"writer-only\";\n" : "")
+        + "    values.put(\"k1\", \"v1\"); values.put(\"k2\", \"v2\");\n"
+        + "    holder.values = values; return holder;\n"
+        + "  }\n"
+        + "}\n";
+  }
+
+  private static String streamSource(boolean writer) {
+    return sourceHeader("StreamPeer")
+        + "  public String marker;\n"
+        + (writer ? "  public String writerOnly;\n" : "")
+        + "  private void writeObject(ObjectOutputStream out) throws 
IOException {\n"
+        + "    out.defaultWriteObject();\n"
+        + "  }\n"
+        + "  private void readObject(ObjectInputStream in)\n"
+        + "      throws IOException, ClassNotFoundException {\n"
+        + "    in.defaultReadObject();\n"
+        + "  }\n"
+        + "  public static StreamPeer newValue() {\n"
+        + "    StreamPeer value = new StreamPeer(); value.marker = 
\"stream-marker\";\n"
+        + (writer ? "    value.writerOnly = \"writer-only\";\n" : "")
+        + "    return value;\n"
+        + "  }\n"
+        + "}\n";
+  }
+
+  private static String sourceHeader(String className) {
+    return "package "
+        + PEER_PACKAGE
+        + ";\n"
+        + "import java.io.*;\n"
+        + "import java.util.*;\n"
+        + "public class "
+        + className
+        + " {\n";
+  }
+
+  public static final class SetHolder {
+    public Set<String> values;
+  }
+
+  public static final class MapHolder {
+    public Map<String, String> values;
+  }
+
+  public static final class CustomSet extends AbstractSet<String> {
+    private LinkedHashSet<String> values = new LinkedHashSet<>();
+    private String marker;
+
+    public CustomSet() {}
+
+    private CustomSet(String marker) {
+      this.marker = marker;
+    }
+
+    public String getMarker() {
+      return marker;
+    }
+
+    @Override
+    public boolean add(String value) {
+      return values.add(value);
+    }
+
+    @Override
+    public Iterator<String> iterator() {
+      return values.iterator();
+    }
+
+    @Override
+    public int size() {
+      return values.size();
+    }
+  }
+
+  public static final class CustomMap extends AbstractMap<String, String> {
+    private LinkedHashMap<String, String> values = new LinkedHashMap<>();
+    private String marker;
+
+    public CustomMap() {}
+
+    private CustomMap(String marker) {
+      this.marker = marker;
+    }
+
+    public String getMarker() {
+      return marker;
+    }
+
+    @Override
+    public String put(String key, String value) {
+      return values.put(key, value);
+    }
+
+    @Override
+    public Set<Entry<String, String>> entrySet() {
+      return values.entrySet();
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to