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 super SerializableField> 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);
}
}