sashapolo commented on a change in pull request #495:
URL: https://github.com/apache/ignite-3/pull/495#discussion_r768717518



##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
##########
@@ -0,0 +1,390 @@
+/*
+ * 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.network.serialization;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory for the user object serialization.
+ */
+public class ClassDescriptorFactory {
+    /**
+     * Factory context.
+     */
+    private final ClassDescriptorFactoryContext context;
+
+    /**
+     * Constructor.
+     *
+     * @param ctx Context.
+     */
+    public ClassDescriptorFactory(ClassDescriptorFactoryContext ctx) {
+        this.context = ctx;
+    }
+
+    /**
+     * Creates the class' descriptor and descriptors of class' fields if 
they're not already created.
+     *
+     * @param clazz Class definition.
+     * @return Class descriptor.
+     */
+    public ClassDescriptor create(Class<?> clazz) {
+        ClassDescriptor classDesc = create0(clazz);
+
+        Map<Integer, ClassDescriptor> localDescs = new HashMap<>();
+        localDescs.put(classDesc.descriptorId(), classDesc);
+
+        Queue<FieldDescriptor> fieldDescriptors = new 
ArrayDeque<>(classDesc.fields());
+
+        while (!fieldDescriptors.isEmpty()) {
+            FieldDescriptor fieldDescriptor = fieldDescriptors.remove();
+
+            int typeDescriptorId = fieldDescriptor.typeDescriptorId();
+
+            if (context.hasDescriptor(typeDescriptorId) || 
localDescs.containsKey(typeDescriptorId)) {
+                continue;
+            }
+
+            Class<?> fieldClass = fieldDescriptor.clazz();
+
+            ClassDescriptor fieldClassDesc = create0(fieldClass);
+
+            localDescs.put(typeDescriptorId, fieldClassDesc);
+
+            fieldDescriptors.addAll(fieldClassDesc.fields());
+        }
+
+        context.addDescriptors(localDescs.values());
+
+        return classDesc;
+    }
+
+    /**
+     * Creates the class' descriptor.
+     *
+     * @param clazz Class.
+     * @return Class' descriptor.
+     */
+    private ClassDescriptor create0(Class<?> clazz) {
+        assert !clazz.isPrimitive() :
+            clazz + " is a primitive, there should be a default descriptor";
+
+        int descriptorId = context.getId(clazz);
+
+        if (Externalizable.class.isAssignableFrom(clazz)) {
+            //noinspection unchecked
+            return externalizable(descriptorId, (Class<? extends 
Externalizable>) clazz);
+        } else if (Serializable.class.isAssignableFrom(clazz)) {
+            //noinspection unchecked
+            return serializable(descriptorId, (Class<? extends Serializable>) 
clazz);
+        } else {
+            return arbitrary(descriptorId, clazz);
+        }
+    }
+
+    /**
+     * Parses the externalizable class definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz        Externalizable class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor externalizable(int descriptorId, Class<? extends 
Externalizable> clazz) {
+        checkHasPublicNoArgConstructor(clazz);
+
+        return new ClassDescriptor(
+            clazz,
+            descriptorId,
+            Collections.emptyList(),
+            SerializationType.EXTERNALIZABLE
+        );
+    }
+
+    /**
+     * Checks if a class has a public no-arg constructor.
+     *
+     * @param clazz Class.
+     */
+    private static void checkHasPublicNoArgConstructor(Class<? extends 
Externalizable> clazz) throws IgniteException {
+        boolean hasPublicNoArgConstructor = true;
+
+        try {
+            Constructor<? extends Externalizable> ctor = 
clazz.getConstructor();
+
+            if (!Modifier.isPublic(ctor.getModifiers())) {
+                hasPublicNoArgConstructor = false;
+            }
+        } catch (NoSuchMethodException e) {
+            hasPublicNoArgConstructor = false;
+        }
+
+        if (!hasPublicNoArgConstructor) {
+            throw new IgniteException(
+                "Externalizable class " + className(clazz) + " has no public 
no-arg constructor");
+        }
+    }
+
+    /**
+     * Parses the serializable class definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz Serializable class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor serializable(int descriptorId, Class<? extends 
Serializable> clazz) {
+        Method writeObject = getWriteObject(clazz);
+        Method readObject = getReadObject(clazz);
+        Method readObjectNoData = getReadObjectNoData(clazz);
+
+        boolean overrideSerialization = writeObject != null && readObject != 
null && readObjectNoData != null;
+
+        Method writeReplace = getWriteReplace(clazz);
+        Method readResolve = getReadResolve(clazz);
+
+        int serializationType = SerializationType.SERIALIZABLE;
+
+        if (overrideSerialization) {
+            serializationType |= SerializationType.SERIALIZABLE_OVERRIDE;
+        }
+
+        if (writeReplace != null) {
+            serializationType |= SerializationType.SERIALIZABLE_WRITE_REPLACE;
+        }
+
+        if (readResolve != null) {
+            serializationType |= SerializationType.SERIALIZABLE_READ_RESOLVE;
+        }
+
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), 
serializationType);
+    }
+
+    /**
+     * Parses the arbitrary class (not serializable or externalizable) 
definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz Arbitrary class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor arbitrary(int descriptorId, Class<?> clazz) {
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), 
SerializationType.ARBITRARY);
+    }
+
+    /**
+     * Gets field descriptors of the class. If a field's type doesn't have an 
id yet, generates it.
+     *
+     * @param clazz Class.
+     * @return List of field descriptor.
+     */
+    private List<FieldDescriptor> fields(Class<?> clazz) {
+        if (clazz.getSuperclass() != Object.class) {
+            // TODO: IGNITE-15945 add support for the inheritance
+            throw new UnsupportedOperationException("IGNITE-15945");
+        }
+
+        return Arrays.stream(clazz.getDeclaredFields())
+            .filter(field -> {
+                int modifiers = field.getModifiers();
+
+                // Ignore static and transient field.
+                return !Modifier.isStatic(modifiers) && 
!Modifier.isTransient(modifiers);
+            })
+            .map(field -> new FieldDescriptor(field, 
context.getId(field.getType())))
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code ANY-ACCESS-MODIFIER Object writeReplace() throws 
ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getWriteReplace(Class<? extends Serializable> clazz) {
+        try {
+            Method writeReplace = clazz.getDeclaredMethod("writeReplace");
+
+            if (!declaresExactExceptions(writeReplace, 
Set.of(ObjectStreamException.class))) {
+                return null;
+            }
+
+            return writeReplace;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code ANY-ACCESS-MODIFIER Object readResolve() throws 
ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getReadResolve(Class<? extends Serializable> clazz) {
+        try {
+            Method readResolve = clazz.getDeclaredMethod("readResolve");
+
+            if (!declaresExactExceptions(readResolve, 
Set.of(ObjectStreamException.class))) {
+                return null;
+            }
+
+            return readResolve;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code private void writeObject(java.io.ObjectOutputStream out) throws 
IOException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getWriteObject(Class<? extends Serializable> clazz) {
+        try {
+            Method writeObject = clazz.getDeclaredMethod("writeObject", 
ObjectOutputStream.class);
+
+            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+                return null;
+            }
+
+            if (!declaresExactExceptions(writeObject, 
Set.of(IOException.class))) {
+                return null;
+            }
+
+            return writeObject;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code private void readObject(java.io.ObjectInputStream in) throws 
IOException,
+     * ClassNotFoundException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getReadObject(Class<? extends Serializable> clazz) {
+        try {
+            Method writeObject = clazz.getDeclaredMethod("readObject", 
ObjectInputStream.class);
+
+            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+                return null;
+            }
+
+            if (!declaresExactExceptions(writeObject, 
Set.of(IOException.class, ClassNotFoundException.class))) {
+                return null;
+            }
+
+            return writeObject;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code private void readObjectNoData() throws ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getReadObjectNoData(Class<? extends Serializable> clazz) {
+        try {
+            Method writeObject = clazz.getDeclaredMethod("readObjectNoData");
+
+            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+                return null;
+            }
+
+            if (!declaresExactExceptions(writeObject, 
Set.of(ObjectStreamException.class))) {
+                return null;
+            }
+
+            return writeObject;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the method's declaration contains throwing only 
of
+     * specified exceptions.
+     *
+     * @param method Method.
+     * @param exceptions Set of exceptions.
+     * @return If the method throws exceptions.
+     */
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    private static boolean declaresExactExceptions(Method method, Set<Class<? 
extends Throwable>> exceptions) {
+        Class<?>[] exceptionTypes = method.getExceptionTypes();
+
+        if (exceptionTypes.length != exceptions.size()) {
+            return false;
+        }
+
+        return Arrays.asList(exceptionTypes).containsAll(exceptions);
+    }
+
+    /**
+     * Gets classes name.
+     *
+     * @param clazz Class.
+     * @return Classes name.
+     */
+    public static String className(Class<?> clazz) {

Review comment:
       Why is this method needed?

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
##########
@@ -0,0 +1,390 @@
+/*
+ * 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.network.serialization;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory for the user object serialization.
+ */
+public class ClassDescriptorFactory {
+    /**
+     * Factory context.
+     */
+    private final ClassDescriptorFactoryContext context;
+
+    /**
+     * Constructor.
+     *
+     * @param ctx Context.
+     */
+    public ClassDescriptorFactory(ClassDescriptorFactoryContext ctx) {
+        this.context = ctx;
+    }
+
+    /**
+     * Creates the class' descriptor and descriptors of class' fields if 
they're not already created.
+     *
+     * @param clazz Class definition.
+     * @return Class descriptor.
+     */
+    public ClassDescriptor create(Class<?> clazz) {
+        ClassDescriptor classDesc = create0(clazz);
+
+        Map<Integer, ClassDescriptor> localDescs = new HashMap<>();
+        localDescs.put(classDesc.descriptorId(), classDesc);
+
+        Queue<FieldDescriptor> fieldDescriptors = new 
ArrayDeque<>(classDesc.fields());
+
+        while (!fieldDescriptors.isEmpty()) {
+            FieldDescriptor fieldDescriptor = fieldDescriptors.remove();
+
+            int typeDescriptorId = fieldDescriptor.typeDescriptorId();
+
+            if (context.hasDescriptor(typeDescriptorId) || 
localDescs.containsKey(typeDescriptorId)) {

Review comment:
       small optimization: check the `localDescs` first to possibly avoid a 
volatile read in `hasDescriptor`

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
##########
@@ -0,0 +1,390 @@
+/*
+ * 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.network.serialization;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory for the user object serialization.
+ */
+public class ClassDescriptorFactory {
+    /**
+     * Factory context.
+     */
+    private final ClassDescriptorFactoryContext context;
+
+    /**
+     * Constructor.
+     *
+     * @param ctx Context.
+     */
+    public ClassDescriptorFactory(ClassDescriptorFactoryContext ctx) {
+        this.context = ctx;
+    }
+
+    /**
+     * Creates the class' descriptor and descriptors of class' fields if 
they're not already created.
+     *
+     * @param clazz Class definition.
+     * @return Class descriptor.
+     */
+    public ClassDescriptor create(Class<?> clazz) {
+        ClassDescriptor classDesc = create0(clazz);
+
+        Map<Integer, ClassDescriptor> localDescs = new HashMap<>();
+        localDescs.put(classDesc.descriptorId(), classDesc);
+
+        Queue<FieldDescriptor> fieldDescriptors = new 
ArrayDeque<>(classDesc.fields());
+
+        while (!fieldDescriptors.isEmpty()) {
+            FieldDescriptor fieldDescriptor = fieldDescriptors.remove();
+
+            int typeDescriptorId = fieldDescriptor.typeDescriptorId();
+
+            if (context.hasDescriptor(typeDescriptorId) || 
localDescs.containsKey(typeDescriptorId)) {
+                continue;
+            }
+
+            Class<?> fieldClass = fieldDescriptor.clazz();
+
+            ClassDescriptor fieldClassDesc = create0(fieldClass);
+
+            localDescs.put(typeDescriptorId, fieldClassDesc);
+
+            fieldDescriptors.addAll(fieldClassDesc.fields());
+        }
+
+        context.addDescriptors(localDescs.values());
+
+        return classDesc;
+    }
+
+    /**
+     * Creates the class' descriptor.
+     *
+     * @param clazz Class.
+     * @return Class' descriptor.
+     */
+    private ClassDescriptor create0(Class<?> clazz) {
+        assert !clazz.isPrimitive() :
+            clazz + " is a primitive, there should be a default descriptor";
+
+        int descriptorId = context.getId(clazz);
+
+        if (Externalizable.class.isAssignableFrom(clazz)) {
+            //noinspection unchecked
+            return externalizable(descriptorId, (Class<? extends 
Externalizable>) clazz);
+        } else if (Serializable.class.isAssignableFrom(clazz)) {
+            //noinspection unchecked
+            return serializable(descriptorId, (Class<? extends Serializable>) 
clazz);
+        } else {
+            return arbitrary(descriptorId, clazz);
+        }
+    }
+
+    /**
+     * Parses the externalizable class definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz        Externalizable class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor externalizable(int descriptorId, Class<? extends 
Externalizable> clazz) {
+        checkHasPublicNoArgConstructor(clazz);
+
+        return new ClassDescriptor(
+            clazz,
+            descriptorId,
+            Collections.emptyList(),
+            SerializationType.EXTERNALIZABLE
+        );
+    }
+
+    /**
+     * Checks if a class has a public no-arg constructor.
+     *
+     * @param clazz Class.
+     */
+    private static void checkHasPublicNoArgConstructor(Class<? extends 
Externalizable> clazz) throws IgniteException {
+        boolean hasPublicNoArgConstructor = true;
+
+        try {
+            Constructor<? extends Externalizable> ctor = 
clazz.getConstructor();
+
+            if (!Modifier.isPublic(ctor.getModifiers())) {
+                hasPublicNoArgConstructor = false;
+            }
+        } catch (NoSuchMethodException e) {
+            hasPublicNoArgConstructor = false;
+        }
+
+        if (!hasPublicNoArgConstructor) {
+            throw new IgniteException(
+                "Externalizable class " + className(clazz) + " has no public 
no-arg constructor");
+        }
+    }
+
+    /**
+     * Parses the serializable class definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz Serializable class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor serializable(int descriptorId, Class<? extends 
Serializable> clazz) {
+        Method writeObject = getWriteObject(clazz);
+        Method readObject = getReadObject(clazz);
+        Method readObjectNoData = getReadObjectNoData(clazz);
+
+        boolean overrideSerialization = writeObject != null && readObject != 
null && readObjectNoData != null;
+
+        Method writeReplace = getWriteReplace(clazz);
+        Method readResolve = getReadResolve(clazz);
+
+        int serializationType = SerializationType.SERIALIZABLE;
+
+        if (overrideSerialization) {
+            serializationType |= SerializationType.SERIALIZABLE_OVERRIDE;
+        }
+
+        if (writeReplace != null) {
+            serializationType |= SerializationType.SERIALIZABLE_WRITE_REPLACE;
+        }
+
+        if (readResolve != null) {
+            serializationType |= SerializationType.SERIALIZABLE_READ_RESOLVE;
+        }
+
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), 
serializationType);
+    }
+
+    /**
+     * Parses the arbitrary class (not serializable or externalizable) 
definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz Arbitrary class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor arbitrary(int descriptorId, Class<?> clazz) {
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), 
SerializationType.ARBITRARY);
+    }
+
+    /**
+     * Gets field descriptors of the class. If a field's type doesn't have an 
id yet, generates it.
+     *
+     * @param clazz Class.
+     * @return List of field descriptor.
+     */
+    private List<FieldDescriptor> fields(Class<?> clazz) {
+        if (clazz.getSuperclass() != Object.class) {
+            // TODO: IGNITE-15945 add support for the inheritance
+            throw new UnsupportedOperationException("IGNITE-15945");
+        }
+
+        return Arrays.stream(clazz.getDeclaredFields())
+            .filter(field -> {
+                int modifiers = field.getModifiers();
+
+                // Ignore static and transient field.
+                return !Modifier.isStatic(modifiers) && 
!Modifier.isTransient(modifiers);
+            })
+            .map(field -> new FieldDescriptor(field, 
context.getId(field.getType())))
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code ANY-ACCESS-MODIFIER Object writeReplace() throws 
ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getWriteReplace(Class<? extends Serializable> clazz) {

Review comment:
       This method and methods below can be `static`

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
##########
@@ -0,0 +1,390 @@
+/*
+ * 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.network.serialization;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory for the user object serialization.
+ */
+public class ClassDescriptorFactory {
+    /**
+     * Factory context.
+     */
+    private final ClassDescriptorFactoryContext context;
+
+    /**
+     * Constructor.
+     *
+     * @param ctx Context.
+     */
+    public ClassDescriptorFactory(ClassDescriptorFactoryContext ctx) {
+        this.context = ctx;
+    }
+
+    /**
+     * Creates the class' descriptor and descriptors of class' fields if 
they're not already created.
+     *
+     * @param clazz Class definition.
+     * @return Class descriptor.
+     */
+    public ClassDescriptor create(Class<?> clazz) {
+        ClassDescriptor classDesc = create0(clazz);
+
+        Map<Integer, ClassDescriptor> localDescs = new HashMap<>();
+        localDescs.put(classDesc.descriptorId(), classDesc);
+
+        Queue<FieldDescriptor> fieldDescriptors = new 
ArrayDeque<>(classDesc.fields());
+
+        while (!fieldDescriptors.isEmpty()) {
+            FieldDescriptor fieldDescriptor = fieldDescriptors.remove();
+
+            int typeDescriptorId = fieldDescriptor.typeDescriptorId();
+
+            if (context.hasDescriptor(typeDescriptorId) || 
localDescs.containsKey(typeDescriptorId)) {
+                continue;
+            }
+
+            Class<?> fieldClass = fieldDescriptor.clazz();
+
+            ClassDescriptor fieldClassDesc = create0(fieldClass);
+
+            localDescs.put(typeDescriptorId, fieldClassDesc);
+
+            fieldDescriptors.addAll(fieldClassDesc.fields());
+        }
+
+        context.addDescriptors(localDescs.values());

Review comment:
       It's a little bit strange that you collect everything into a map and 
then collect everything into a map again inside `addDescriptors`...

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationType.java
##########
@@ -0,0 +1,44 @@
+/*
+ * 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.network.serialization;
+
+/**
+ * Serialization type flags.
+ */
+public class SerializationType {
+    /** Used for predefined descriptors like primitive (or boxed int). See 
{@link DefaultType}. */
+    public static final int DEFAULT = 0b0;

Review comment:
       It is more common to use shifts for declaring flags, for example:
   ```
   public static final int SERIALIZABLE_OVERRIDE = 1 << 3;
   public static final int SERIALIZABLE = 1 << 4;
   ```

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryContext.java
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.network.serialization;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory context.
+ */
+public class ClassDescriptorFactoryContext {
+    /** Quantity of descriptor ids reserved for the default descriptors. */
+    public static final int DEFAULT_DESCRIPTORS_OFFSET_COUNT = 1000;
+
+    /** Sequential id generator for class descriptors. */
+    private final AtomicInteger idGenerator = new 
AtomicInteger(DEFAULT_DESCRIPTORS_OFFSET_COUNT);
+
+    /** Map class -> descriptor id. */
+    private final ConcurrentMap<Class<?>, Integer> idMap = new 
ConcurrentHashMap<>();
+
+    /** Map descriptor id -> class descriptor. */
+    private final ConcurrentMap<Integer, ClassDescriptor> descriptorMap = new 
ConcurrentHashMap<>();
+
+    /**
+     * Constructor.
+     */
+    public ClassDescriptorFactoryContext() {
+        for (DefaultType value : DefaultType.values()) {
+            addPredefinedDescriptor(value.clazz(), value.asClassDescriptor());
+        }
+    }
+
+    /**
+     * Adds predefined class descriptor with a statically configured id.
+     *
+     * @param clazz Class.
+     * @param descriptor Descriptor.
+     */
+    private void addPredefinedDescriptor(Class<?> clazz, ClassDescriptor 
descriptor) {
+        int descriptorId = descriptor.descriptorId();
+
+        Integer existingId = idMap.put(clazz, descriptorId);
+
+        assert existingId == null;
+
+        ClassDescriptor existingDescriptor = descriptorMap.put(descriptorId, 
descriptor);
+
+        assert existingDescriptor == null;
+    }
+
+    /**
+     * Gets descriptor id for the class.
+     *
+     * @param clazz Class.
+     * @return Descriptor id.
+     */
+    public int getId(Class<?> clazz) {
+        return idMap.computeIfAbsent(clazz, unused -> 
idGenerator.getAndIncrement());
+    }
+
+    /**
+     * Gets a descriptor by the id.
+     *
+     * @param descriptorId Descriptor id.
+     * @return Descriptor.
+     */
+    @Nullable
+    public ClassDescriptor getDescriptor(int descriptorId) {
+        return descriptorMap.get(descriptorId);
+    }
+
+    /**
+     * Gets a descriptor by the class.
+     *
+     * @param clazz Class.
+     * @return Descriptor.
+     */
+    @Nullable
+    public ClassDescriptor getDescriptor(Class<?> clazz) {
+        Integer descriptorId = idMap.get(clazz);
+
+        if (descriptorId == null) {
+            return null;
+        }
+
+        return descriptorMap.get(descriptorId);
+    }
+
+    /**
+     * Returns {@code true} if there is a descriptor for the id.
+     *
+     * @param descriptorId Descriptor id.
+     * @return {@code true} if there is a descriptor for the id.
+     */
+    public boolean hasDescriptor(int descriptorId) {
+        return descriptorMap.containsKey(descriptorId);
+    }
+
+    /**
+     * Adds a list of descriptors.
+     *
+     * @param descriptors Descriptors.
+     */
+    public void addDescriptors(Collection<ClassDescriptor> descriptors) {
+        Map<Integer, ClassDescriptor> tmpDescriptorMap = new HashMap<>();
+
+        for (ClassDescriptor descriptor : descriptors) {
+            Integer descriptorId = idMap.get(descriptor.clazz());
+
+            assert descriptorId != null : "Attempting to store unregistered 
descriptor";
+
+            int realDescriptorId = descriptor.descriptorId();
+
+            assert descriptorId == realDescriptorId : "Descriptor id doesn't 
match, registered="
+                + descriptorId + ", real=" + realDescriptorId;
+
+            tmpDescriptorMap.put(descriptorId, descriptor);
+        }
+
+        descriptorMap.putAll(tmpDescriptorMap);

Review comment:
       This method is not atomic

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryContext.java
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.network.serialization;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory context.
+ */
+public class ClassDescriptorFactoryContext {
+    /** Quantity of descriptor ids reserved for the default descriptors. */
+    public static final int DEFAULT_DESCRIPTORS_OFFSET_COUNT = 1000;

Review comment:
       ```suggestion
       private static final int DEFAULT_DESCRIPTORS_OFFSET_COUNT = 1000;
   ```

##########
File path: 
modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
##########
@@ -0,0 +1,390 @@
+/*
+ * 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.network.serialization;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class descriptor factory for the user object serialization.
+ */
+public class ClassDescriptorFactory {
+    /**
+     * Factory context.
+     */
+    private final ClassDescriptorFactoryContext context;
+
+    /**
+     * Constructor.
+     *
+     * @param ctx Context.
+     */
+    public ClassDescriptorFactory(ClassDescriptorFactoryContext ctx) {
+        this.context = ctx;
+    }
+
+    /**
+     * Creates the class' descriptor and descriptors of class' fields if 
they're not already created.
+     *
+     * @param clazz Class definition.
+     * @return Class descriptor.
+     */
+    public ClassDescriptor create(Class<?> clazz) {
+        ClassDescriptor classDesc = create0(clazz);
+
+        Map<Integer, ClassDescriptor> localDescs = new HashMap<>();
+        localDescs.put(classDesc.descriptorId(), classDesc);
+
+        Queue<FieldDescriptor> fieldDescriptors = new 
ArrayDeque<>(classDesc.fields());
+
+        while (!fieldDescriptors.isEmpty()) {
+            FieldDescriptor fieldDescriptor = fieldDescriptors.remove();
+
+            int typeDescriptorId = fieldDescriptor.typeDescriptorId();
+
+            if (context.hasDescriptor(typeDescriptorId) || 
localDescs.containsKey(typeDescriptorId)) {
+                continue;
+            }
+
+            Class<?> fieldClass = fieldDescriptor.clazz();
+
+            ClassDescriptor fieldClassDesc = create0(fieldClass);
+
+            localDescs.put(typeDescriptorId, fieldClassDesc);
+
+            fieldDescriptors.addAll(fieldClassDesc.fields());
+        }
+
+        context.addDescriptors(localDescs.values());
+
+        return classDesc;
+    }
+
+    /**
+     * Creates the class' descriptor.
+     *
+     * @param clazz Class.
+     * @return Class' descriptor.
+     */
+    private ClassDescriptor create0(Class<?> clazz) {
+        assert !clazz.isPrimitive() :
+            clazz + " is a primitive, there should be a default descriptor";
+
+        int descriptorId = context.getId(clazz);
+
+        if (Externalizable.class.isAssignableFrom(clazz)) {
+            //noinspection unchecked
+            return externalizable(descriptorId, (Class<? extends 
Externalizable>) clazz);
+        } else if (Serializable.class.isAssignableFrom(clazz)) {
+            //noinspection unchecked
+            return serializable(descriptorId, (Class<? extends Serializable>) 
clazz);
+        } else {
+            return arbitrary(descriptorId, clazz);
+        }
+    }
+
+    /**
+     * Parses the externalizable class definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz        Externalizable class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor externalizable(int descriptorId, Class<? extends 
Externalizable> clazz) {
+        checkHasPublicNoArgConstructor(clazz);
+
+        return new ClassDescriptor(
+            clazz,
+            descriptorId,
+            Collections.emptyList(),
+            SerializationType.EXTERNALIZABLE
+        );
+    }
+
+    /**
+     * Checks if a class has a public no-arg constructor.
+     *
+     * @param clazz Class.
+     */
+    private static void checkHasPublicNoArgConstructor(Class<? extends 
Externalizable> clazz) throws IgniteException {
+        boolean hasPublicNoArgConstructor = true;
+
+        try {
+            Constructor<? extends Externalizable> ctor = 
clazz.getConstructor();
+
+            if (!Modifier.isPublic(ctor.getModifiers())) {
+                hasPublicNoArgConstructor = false;
+            }
+        } catch (NoSuchMethodException e) {
+            hasPublicNoArgConstructor = false;
+        }
+
+        if (!hasPublicNoArgConstructor) {
+            throw new IgniteException(
+                "Externalizable class " + className(clazz) + " has no public 
no-arg constructor");
+        }
+    }
+
+    /**
+     * Parses the serializable class definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz Serializable class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor serializable(int descriptorId, Class<? extends 
Serializable> clazz) {
+        Method writeObject = getWriteObject(clazz);
+        Method readObject = getReadObject(clazz);
+        Method readObjectNoData = getReadObjectNoData(clazz);
+
+        boolean overrideSerialization = writeObject != null && readObject != 
null && readObjectNoData != null;
+
+        Method writeReplace = getWriteReplace(clazz);
+        Method readResolve = getReadResolve(clazz);
+
+        int serializationType = SerializationType.SERIALIZABLE;
+
+        if (overrideSerialization) {
+            serializationType |= SerializationType.SERIALIZABLE_OVERRIDE;
+        }
+
+        if (writeReplace != null) {
+            serializationType |= SerializationType.SERIALIZABLE_WRITE_REPLACE;
+        }
+
+        if (readResolve != null) {
+            serializationType |= SerializationType.SERIALIZABLE_READ_RESOLVE;
+        }
+
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), 
serializationType);
+    }
+
+    /**
+     * Parses the arbitrary class (not serializable or externalizable) 
definition.
+     *
+     * @param descriptorId Descriptor id of the class.
+     * @param clazz Arbitrary class.
+     * @return Class descriptor.
+     */
+    private ClassDescriptor arbitrary(int descriptorId, Class<?> clazz) {
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), 
SerializationType.ARBITRARY);
+    }
+
+    /**
+     * Gets field descriptors of the class. If a field's type doesn't have an 
id yet, generates it.
+     *
+     * @param clazz Class.
+     * @return List of field descriptor.
+     */
+    private List<FieldDescriptor> fields(Class<?> clazz) {
+        if (clazz.getSuperclass() != Object.class) {
+            // TODO: IGNITE-15945 add support for the inheritance
+            throw new UnsupportedOperationException("IGNITE-15945");
+        }
+
+        return Arrays.stream(clazz.getDeclaredFields())
+            .filter(field -> {
+                int modifiers = field.getModifiers();
+
+                // Ignore static and transient field.
+                return !Modifier.isStatic(modifiers) && 
!Modifier.isTransient(modifiers);
+            })
+            .map(field -> new FieldDescriptor(field, 
context.getId(field.getType())))
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code ANY-ACCESS-MODIFIER Object writeReplace() throws 
ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getWriteReplace(Class<? extends Serializable> clazz) {
+        try {
+            Method writeReplace = clazz.getDeclaredMethod("writeReplace");
+
+            if (!declaresExactExceptions(writeReplace, 
Set.of(ObjectStreamException.class))) {
+                return null;
+            }
+
+            return writeReplace;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code ANY-ACCESS-MODIFIER Object readResolve() throws 
ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getReadResolve(Class<? extends Serializable> clazz) {
+        try {
+            Method readResolve = clazz.getDeclaredMethod("readResolve");
+
+            if (!declaresExactExceptions(readResolve, 
Set.of(ObjectStreamException.class))) {
+                return null;
+            }
+
+            return readResolve;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code private void writeObject(java.io.ObjectOutputStream out) throws 
IOException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getWriteObject(Class<? extends Serializable> clazz) {
+        try {
+            Method writeObject = clazz.getDeclaredMethod("writeObject", 
ObjectOutputStream.class);
+
+            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+                return null;
+            }
+
+            if (!declaresExactExceptions(writeObject, 
Set.of(IOException.class))) {
+                return null;
+            }
+
+            return writeObject;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code private void readObject(java.io.ObjectInputStream in) throws 
IOException,
+     * ClassNotFoundException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getReadObject(Class<? extends Serializable> clazz) {
+        try {
+            Method writeObject = clazz.getDeclaredMethod("readObject", 
ObjectInputStream.class);
+
+            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+                return null;
+            }
+
+            if (!declaresExactExceptions(writeObject, 
Set.of(IOException.class, ClassNotFoundException.class))) {
+                return null;
+            }
+
+            return writeObject;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a method with the signature
+     * {@code private void readObjectNoData() throws ObjectStreamException}.
+     *
+     * @param clazz Class.
+     * @return Method.
+     */
+    @Nullable
+    private Method getReadObjectNoData(Class<? extends Serializable> clazz) {
+        try {
+            Method writeObject = clazz.getDeclaredMethod("readObjectNoData");
+
+            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+                return null;
+            }
+
+            if (!declaresExactExceptions(writeObject, 
Set.of(ObjectStreamException.class))) {
+                return null;
+            }
+
+            return writeObject;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the method's declaration contains throwing only 
of
+     * specified exceptions.
+     *
+     * @param method Method.
+     * @param exceptions Set of exceptions.
+     * @return If the method throws exceptions.
+     */
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    private static boolean declaresExactExceptions(Method method, Set<Class<? 
extends Throwable>> exceptions) {
+        Class<?>[] exceptionTypes = method.getExceptionTypes();
+
+        if (exceptionTypes.length != exceptions.size()) {
+            return false;
+        }
+
+        return Arrays.asList(exceptionTypes).containsAll(exceptions);
+    }
+
+    /**
+     * Gets classes name.
+     *
+     * @param clazz Class.
+     * @return Classes name.
+     */
+    public static String className(Class<?> clazz) {
+        return clazz.getName();
+    }
+
+    /**
+     * Returns {@code true} if the class is final.
+     *
+     * @param clazz Class.
+     * @return {@code true} if the class is final.
+     */
+    public static boolean isFinal(Class<?> clazz) {

Review comment:
       This method is never used in this class, it should be removed




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to