All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.marshalling.reflect.SerializableClass Maven / Gradle / Ivy

The newest version!
/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.jboss.marshalling.reflect;

import static java.lang.System.getSecurityManager;
import static java.security.AccessController.doPrivileged;

import java.io.ObjectInput;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.Field;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.ObjectStreamField;
import java.io.ObjectStreamClass;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;

import org.jboss.marshalling._private.GetUnsafeAction;
import sun.misc.Unsafe;

/**
 * Reflection information about a serializable class.  Intended for use by implementations of the Marshalling API.
 *
 * @author Richard Opalka
 */
public final class SerializableClass {
    private static final Unsafe unsafe = getSecurityManager() == null ? GetUnsafeAction.INSTANCE.run() : doPrivileged(GetUnsafeAction.INSTANCE);
    private static final SerializableClassRegistry REGISTRY = SerializableClassRegistry.getInstanceUnchecked();
    private final IdentityHashMap, Constructor> nonInitConstructors;
    private final Class subject;
    private final SerMethods serMethods;
    private final SerializableField[] fields;
    private final Map fieldsByName;
    private final long effectiveSerialVersionUID;
    private final boolean isRecord;

    private static final Comparator NAME_COMPARATOR = new Comparator() {
        public int compare(final SerializableField o1, final SerializableField o2) {
            return o1.getName().compareTo(o2.getName());
        }
    };
    /**
     * An empty array of fields.
     */
    public static final SerializableField[] NOFIELDS = new SerializableField[0];
    private static final IdentityHashMap, Constructor> EMPTY_IHM = new IdentityHashMap<>(0);

    SerializableClass(Class subject) {
        this.subject = subject;
        isRecord = JDKSpecific.isRecord(subject);
        if (isRecord) {
            nonInitConstructors = EMPTY_IHM;
        } else {
            final IdentityHashMap, Constructor> constructorMap = new IdentityHashMap, Constructor>();
            for (Class t = subject.getSuperclass(); t != null; t = t.getSuperclass()) {
                final SerializableClass lookedUp = REGISTRY.lookup(t);
                final Constructor constructor = lookedUp.serMethods.getNoArgConstructor();
                if (constructor != null) {
                    constructorMap.put(t, SerMethods.newConstructorForSerialization(subject, constructor));
                }
            }
            nonInitConstructors = constructorMap;
        }
        // private methods
        serMethods = new SerMethods(subject);
        final ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(subject);
        effectiveSerialVersionUID = objectStreamClass == null ? 0L : objectStreamClass.getSerialVersionUID(); // todo find a better solution
        if (subject.getSimpleName().indexOf('/') != -1) {
            // it's a hidden class
            fields = NOFIELDS;
            this.fieldsByName = Collections.emptyMap();
        } else {
            final HashMap fieldsByName = new HashMap();
            for (SerializableField serializableField : fields = getSerializableFields(subject, isRecord)) {
                fieldsByName.put(serializableField.getName(), serializableField);
            }
            this.fieldsByName = fieldsByName;
        }
    }

    private static SerializableField[] getSerializableFields(Class clazz, boolean isRecord) {
        final Field[] declaredFields = clazz.getDeclaredFields();
        if (isRecord) {
            final Map map = new HashMap<>();
            for (Field field : declaredFields) {
                map.put(field.getName(), field);
            }
            SerializableField.RecordComponent[] recordComponents = JDKSpecific.getRecordComponents(clazz);
            SerializableField[] fields = new SerializableField[recordComponents.length];
            for (int i = 0; i < recordComponents.length; i++) {
                Field field = map.get(recordComponents[i].getName());
                fields[i] = new SerializableField(field.getType(), field.getName(), false, field, recordComponents[i]);
            }
            Arrays.sort(fields, NAME_COMPARATOR);
            return fields;
        }
        final ObjectStreamField[] objectStreamFields = getDeclaredSerialPersistentFields(clazz);
        if (objectStreamFields != null) {
            final Map map = new HashMap<>();
            for (Field field : declaredFields) {
                map.put(field.getName(), field);
            }
            SerializableField[] fields = new SerializableField[objectStreamFields.length];
            for (int i = 0; i < objectStreamFields.length; i++) {
                ObjectStreamField field = objectStreamFields[i];
                final String name = field.getName();
                final Field realField = map.get(name);
                if (realField != null && realField.getType() == field.getType()) {
                    // allow direct updating of the field data since the types match
                    fields[i] = new SerializableField(field.getType(), name, field.isUnshared(), realField, null);
                } else {
                    // no direct update possible
                    fields[i] = new SerializableField(field.getType(), name, field.isUnshared(), null, null);
                }
            }
            Arrays.sort(fields, NAME_COMPARATOR);
            return fields;
        }
        final ArrayList fields = new ArrayList<>(declaredFields.length);
        for (Field field : declaredFields) {
            if ((field.getModifiers() & (Modifier.TRANSIENT | Modifier.STATIC)) == 0) {
                fields.add(new SerializableField(field.getType(), field.getName(), false, field, null));
            }
        }
        Collections.sort(fields, NAME_COMPARATOR);
        return fields.toArray(new SerializableField[0]);
    }

    private static ObjectStreamField[] getDeclaredSerialPersistentFields(Class clazz) {
        final Field field;
        try {
            field = clazz.getDeclaredField("serialPersistentFields");
        } catch (NoSuchFieldException e) {
            return null;
        }
        if (field == null) {
            return null;
        }
        final int requiredModifiers = Modifier.STATIC | Modifier.PRIVATE | Modifier.FINAL;
        if ((field.getModifiers() & requiredModifiers) != requiredModifiers) {
            return null;
        }
        try {
            return (ObjectStreamField[]) unsafe.getObject(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field));
        } catch (ClassCastException e) {
            return null;
        }
    }

    /**
     * Get the serializable fields of this class.  The returned array is a direct reference, so care should be taken
     * not to modify it.
     *
     * @return the fields
     */
    public SerializableField[] getFields() {
        return fields;
    }

    /**
     * Create a synthetic field for this object class.
     *
     * @param name the name of the field
     * @param fieldType the field type
     * @param unshared {@code true} if the field should be unshared
     * @return the field
     * @throws ClassNotFoundException if a class was not found while looking up the subject class
     */
    public SerializableField getSerializableField(String name, Class fieldType, boolean unshared) throws ClassNotFoundException {
        final SerializableField serializableField = fieldsByName.get(name);
        if (serializableField != null) {
            return serializableField;
        }
        return new SerializableField(fieldType, name, unshared, null, null);
    }

    /**
     * Find a field for this object class by name.
     *
     * @param name the name of the field
     * @return the field or {@code null} if there is one with that name
     * @throws ClassNotFoundException if a class was not found while looking up the subject class
     */
    public SerializableField getSerializableFieldByName(String name) throws ClassNotFoundException {
        return fieldsByName.get(name);
    }

    /**
     * Determine whether this class has a {@code writeObject()} method.
     *
     * @return {@code true} if there is a {@code writeObject()} method
     */
    public boolean hasWriteObject() {
        return serMethods.hasWriteObject();
    }

    /**
     * Invoke the {@code writeObject()} method for an object.
     *
     * @param object the object to invoke on
     * @param outputStream the object output stream to pass in
     * @throws IOException if an I/O error occurs
     */
    public void callWriteObject(Object object, ObjectOutputStream outputStream) throws IOException {
        serMethods.callWriteObject(object, outputStream);
    }

    /**
     * Determine whether this class has a {@code readObject()} method.
     *
     * @return {@code true} if there is a {@code readObject()} method
     */
    public boolean hasReadObject() {
        return serMethods.hasReadObject();
    }

    /**
     * Invoke the {@code readObject()} method for an object.
     *
     * @param object the object to invoke on
     * @param inputStream the object input stream to pass in
     * @throws IOException if an I/O error occurs
     * @throws ClassNotFoundException if a class was not able to be loaded
     */
    public void callReadObject(Object object, ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
        serMethods.callReadObject(object, inputStream);
    }

    /**
     * Determine whether this class has a {@code readObjectNoData()} method.
     *
     * @return {@code true} if there is a {@code readObjectNoData()} method
     */
    public boolean hasReadObjectNoData() {
        return serMethods.hasReadObjectNoData();
    }

    /**
     * Invoke the {@code readObjectNoData()} method for an object.
     *
     * @param object the object to invoke on
     * @throws ObjectStreamException if an I/O error occurs
     */
    public void callReadObjectNoData(Object object) throws ObjectStreamException {
        serMethods.callReadObjectNoData(object);
    }

    /**
     * Determine whether this class has a {@code writeReplace()} method.
     *
     * @return {@code true} if there is a {@code writeReplace()} method
     */
    public boolean hasWriteReplace() {
        return serMethods.hasWriteReplace();
    }

    /**
     * Invoke the {@code writeReplace()} method for an object.
     *
     * @param object the object to invoke on
     * @return the nominated replacement object 
     * @throws ObjectStreamException if an I/O error occurs
     */
    public Object callWriteReplace(Object object) throws ObjectStreamException {
        return serMethods.callWriteReplace(object);
    }

    /**
     * Determine whether this class has a {@code readResolve()} method.
     *
     * @return {@code true} if there is a {@code readResolve()} method
     */
    public boolean hasReadResolve() {
        return serMethods.hasReadResolve();
    }

    /**
     * Invoke the {@code readResolve()} method for an object.
     *
     * @param object the object to invoke on
     * @return the original object
     * @throws ObjectStreamException if an I/O error occurs
     */
    public Object callReadResolve(Object object) throws ObjectStreamException {
        return serMethods.callReadResolve(object);
    }

    /**
     * Determine whether this class has a public no-arg constructor.
     *
     * @return {@code true} if there is such a constructor
     */
    public boolean hasPublicNoArgConstructor() {
        final Constructor noArgConstructor = serMethods.getNoArgConstructor();
        return noArgConstructor != null && Modifier.isPublic(noArgConstructor.getModifiers());
    }

    /**
     * Invoke the no-arg constructor on this class.
     *
     * @return the new instance
     * @throws IOException if an I/O error occurs
     */
    public Object callNoArgConstructor() throws IOException {
        final Constructor noArgConstructor = serMethods.getNoArgConstructor();
        return invokeConstructor(noArgConstructor);
    }

    /**
     * Determine whether this class has a public constructor accepting an ObjectInput.
     *
     * @return {@code true} if there is such a constructor
     */
    public boolean hasObjectInputConstructor() {
        final Constructor objectInputConstructor = serMethods.getObjectInputConstructor();
        return objectInputConstructor != null && Modifier.isPublic(objectInputConstructor.getModifiers());
    }

    /**
     * Invoke the public constructor accepting an ObjectInput.
     *
     * @param objectInput the ObjectInput to pass to the constructor
     * @return the new instance
     * @throws IOException if an I/O error occurs
     */
    public Object callObjectInputConstructor(final ObjectInput objectInput) throws IOException {
        final Constructor objectInputConstructor = serMethods.getObjectInputConstructor();
        return invokeConstructor(objectInputConstructor, objectInput);
    }

    /**
     * Determine whether this class has a non-init constructor.
     *
     * @return whether this class has a non-init constructor
     */
    public boolean hasNoInitConstructor(Class target) {
        return nonInitConstructors.containsKey(target);
    }

    /**
     * Invoke the non-init constructor on this class.
     *
     * @return the new instance
     */
    public Object callNonInitConstructor(Class target) {
        return invokeConstructorNoException(nonInitConstructors.get(target));
    }

    private static  T invokeConstructor(Constructor constructor, Object... args) throws IOException {
        if (constructor == null) {
            throw new IllegalArgumentException("No matching constructor");
        }
        try {
            return constructor.newInstance(args);
        } catch (InvocationTargetException e) {
            final Throwable te = e.getTargetException();
            if (te instanceof IOException) {
                throw (IOException)te;
            } else if (te instanceof RuntimeException) {
                throw (RuntimeException)te;
            } else if (te instanceof Error) {
                throw (Error)te;
            } else {
                throw new IllegalStateException("Unexpected exception", te);
            }
        } catch (InstantiationException e) {
            throw new IllegalStateException("Instantiation failed unexpectedly");
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Constructor is unexpectedly inaccessible");
        }
    }

    private static  T invokeConstructorNoException(Constructor constructor, Object... args) {
        if (constructor == null) {
            throw new IllegalArgumentException("No matching constructor");
        }
        try {
            return constructor.newInstance(args);
        } catch (InvocationTargetException e) {
            final Throwable te = e.getTargetException();
            if (te instanceof RuntimeException) {
                throw (RuntimeException)te;
            } else if (te instanceof Error) {
                throw (Error)te;
            } else {
                throw new IllegalStateException("Unexpected exception", te);
            }
        } catch (InstantiationException e) {
            throw new IllegalStateException("Instantiation failed unexpectedly");
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Constructor is unexpectedly inaccessible");
        }
    }

    /**
     * Get the effective serial version UID of this class.
     *
     * @return the serial version UID
     */
    public long getEffectiveSerialVersionUID() {
        return effectiveSerialVersionUID;
    }

    /**
     * Get the {@code Class} of this class.
     *
     * @return the subject class
     */
    public Class getSubjectClass() {
        return subject;
    }

    @SuppressWarnings("unchecked")
     Constructor getNoArgConstructor() {
        return (Constructor) serMethods.getNoArgConstructor();
    }

    public String toString() {
        return String.format("Serializable %s", getSubjectClass());
    }

    @SuppressWarnings("unchecked")
     Constructor getNoInitConstructor() {
        Class clazz = getClass();
        while (Serializable.class.isAssignableFrom(clazz)) {
            clazz = clazz.getSuperclass();
        }
        return (Constructor) nonInitConstructors.get(clazz);
    }

    SerMethods getSerMethods() {
        return serMethods;
    }

    /**
     * Determine if this class is a record.
     *
     * @return The record components or null
     */
    public boolean isRecord() {
        return isRecord;
    }

    /**
     * Invokes the record constructor for this class. If the class is not a record
     * or the values are incorrect a IllegalStateException is thrown.
     * @param args The record arguments for the constructor
     * @return The new record created
     */
    public Object invokeRecordCanonicalConstructor(Object[] args) {
        return JDKSpecific.invokeRecordCanonicalConstructor(subject, fields, args);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy