diff --git a/src/java.base/share/classes/java/io/InvalidClassException.java b/src/java.base/share/classes/java/io/InvalidClassException.java index be187597726..5045b0c4566 100644 --- a/src/java.base/share/classes/java/io/InvalidClassException.java +++ b/src/java.base/share/classes/java/io/InvalidClassException.java @@ -37,6 +37,9 @@ * an enum type *
  • Other conditions given in the Java Object Serialization * Specification + *
  • A {@linkplain Class#isValue()} value class implements {@linkplain Serializable} + * but does not delegate to a serialization proxy using {@code writeReplace()}. + *
  • A {@linkplain Class#isValue()} value class implements {@linkplain Externalizable}. * * * @since 1.1 diff --git a/src/java.base/share/classes/java/io/ObjectInputStream.java b/src/java.base/share/classes/java/io/ObjectInputStream.java index 27b7fa0ac56..622b47f0a5e 100644 --- a/src/java.base/share/classes/java/io/ObjectInputStream.java +++ b/src/java.base/share/classes/java/io/ObjectInputStream.java @@ -26,11 +26,13 @@ package java.io; import java.io.ObjectInputFilter.Config; -import java.io.ObjectStreamClass.RecordSupport; +import java.io.ObjectStreamClass.ConstructorSupport; +import java.io.ObjectStreamClass.ClassDataSlot; import java.lang.System.Logger; import java.lang.invoke.MethodHandle; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.nio.charset.StandardCharsets; @@ -40,7 +42,8 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Arrays; -import java.util.Map; +import java.util.List; +import java.util.Locale; import java.util.Objects; import jdk.internal.access.JavaLangAccess; @@ -51,6 +54,7 @@ import sun.reflect.misc.ReflectUtil; import sun.security.action.GetBooleanAction; import sun.security.action.GetIntegerAction; +import sun.security.action.GetPropertyAction; /** * An ObjectInputStream deserializes primitive data and objects previously @@ -246,10 +250,9 @@ * Java Object Serialization Specification, Section 1.13, * "Serialization of Records" for additional information. * - *

    Value objects are deserialized differently than ordinary serializable objects or records. - * See - * Java Object Serialization Specification, Section 1.14, - * "Serialization of Value Objects" for additional information. + *

    Value classes are {@linkplain Serializable} through the use of the serialization proxy pattern. + * See {@linkplain ObjectOutputStream##valueclass-serialization value class serialization} for details. + * When the proxy is deserialized it re-constructs and returns the value object. * * @spec serialization/index.html Java Object Serialization Specification * @author Mike Warres @@ -264,6 +267,16 @@ public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants { + private static final String TRACE_DEST = + GetPropertyAction.privilegedGetProperty("TRACE"); + + static void TRACE(String format, Object... args) { + if (TRACE_DEST != null) { + var ps = "OUT".equals(TRACE_DEST.toUpperCase(Locale.ROOT)) ? System.out : System.err; + ps.println(("TRACE " + format).formatted(args)); + } + } + /** handle value representing null */ private static final int NULL_HANDLE = -1; @@ -473,6 +486,14 @@ protected ObjectInputStream() throws IOException, SecurityException { * each object (regular or class) read to reconstruct the root object. * See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details. * + *

    Serialization and deserialization of value classes is described in + * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}. + * + * @implSpec + * When enabled with {@code --enable-preview}, serialization and deserialization of + * Core Library value classes migrated from pre-JEP 401 identity classes is + * implementation specific. + * *

    Exceptions are thrown for problems with the InputStream and for * classes that should not be deserialized. All exceptions are fatal to * the InputStream and leave it in an indeterminate state; it is up to the @@ -606,6 +627,9 @@ protected Object readObjectOverride() * each object (regular or class) read to reconstruct the root object. * See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details. * + *

    Serialization and deserialization of value classes is described in + * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}. + * *

    ObjectInputStream subclasses which override this method can only be * constructed in security contexts possessing the * "enableSubclassImplementation" SerializablePermission; any attempt to @@ -2259,14 +2283,6 @@ private Object readOrdinaryObject(boolean unshared) throw new InvalidClassException("invalid class descriptor"); } - Object obj; - try { - obj = desc.isInstantiable() ? desc.newInstance() : null; - } catch (Exception ex) { - throw new InvalidClassException(desc.forClass().getName(), - "unable to create instance", ex); - } - // Assign the handle and initially set to null or the unsharedMarker passHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); @@ -2274,72 +2290,123 @@ private Object readOrdinaryObject(boolean unshared) handles.markException(passHandle, resolveEx); } - final boolean isRecord = desc.isRecord(); - if (isRecord) { - assert obj == null; - obj = readRecord(desc); - if (!unshared) - handles.setObject(passHandle, obj); - } else if (desc.isExternalizable()) { - if (desc.isValue()) { - throw new InvalidClassException("Externalizable not valid for value class " - + cl.getName()); - } - if (!unshared) - handles.setObject(passHandle, obj); - readExternalData((Externalizable) obj, desc); - } else if (desc.isValue()) { - if (obj == null) { - throw new InvalidClassException("Serializable not valid for value class " - + cl.getName()); - } - // For value objects, read the fields and finish the buffer before publishing the ref - readSerialData(obj, desc); - obj = desc.finishValue(obj); - if (!unshared) - handles.setObject(passHandle, obj); - } else { - // For all other objects, publish the ref and then read the data - if (!unshared) - handles.setObject(passHandle, obj); - readSerialData(obj, desc); - } + try { + // Dispatch on the factory mode to read an object from the stream. + Object obj = switch (desc.factoryMode()) { + case READ_OBJECT_DEFAULT -> readSerialDefaultObject(desc, unshared); + case READ_OBJECT_CUSTOM -> readSerialCustomData(desc, unshared); + case READ_RECORD -> readRecord(desc, unshared); + case READ_EXTERNALIZABLE -> readExternalObject(desc, unshared); + case READ_OBJECT_VALUE -> readObjectValue(desc, unshared); + case READ_NO_LOCAL_CLASS -> readAbsentLocalClass(desc, unshared); + case null -> throw new AssertionError("Unknown factoryMode for: " + desc.getName(), + resolveEx); + }; - handles.finish(passHandle); + handles.finish(passHandle); - if (obj != null && - handles.lookupException(passHandle) == null && - desc.hasReadResolveMethod()) - { - Object rep = desc.invokeReadResolve(obj); - if (unshared && rep.getClass().isArray()) { - rep = cloneArray(rep); - } - if (rep != obj) { - // Filter the replacement object - if (rep != null) { - if (rep.getClass().isArray()) { - filterCheck(rep.getClass(), Array.getLength(rep)); - } else { - filterCheck(rep.getClass(), -1); + if (obj != null && + handles.lookupException(passHandle) == null && + desc.hasReadResolveMethod()) + { + Object rep = desc.invokeReadResolve(obj); + if (unshared && rep.getClass().isArray()) { + rep = cloneArray(rep); + } + if (rep != obj) { + // Filter the replacement object + if (rep != null) { + if (rep.getClass().isArray()) { + filterCheck(rep.getClass(), Array.getLength(rep)); + } else { + filterCheck(rep.getClass(), -1); + } } + handles.setObject(passHandle, obj = rep); } - handles.setObject(passHandle, obj = rep); } + + return obj; + } catch (UncheckedIOException uioe) { + // Consistent re-throw for nested UncheckedIOExceptions + throw uioe.getCause(); } + } - return obj; + /** + * {@return a value class instance by invoking its constructor with field values read from the stream. + * The fields of the class in the stream are matched to the local fields and applied to + * the constructor. + * If the stream contains superclasses with serializable fields, + * an InvalidClassException is thrown with an incompatible class change message. + * + * @param desc the class descriptor read from the stream, the local class is a value class + * @param unshared if the object is not to be shared + * @throws InvalidClassException if the stream contains a superclass with serializable fields. + * @throws IOException if there are I/O errors while reading from the + * underlying {@code InputStream} + */ + private Object readObjectValue(ObjectStreamClass desc, boolean unshared) throws IOException { + final ObjectStreamClass localDesc = desc.getLocalDesc(); + TRACE("readObjectValue: %s, local class: %s", desc.getName(), localDesc.getName()); + // Check for un-expected fields in superclasses + List slots = desc.getClassDataLayout(); + for (int i = 0; i < slots.size()-1; i++) { + ClassDataSlot slot = slots.get(i); + if (slot.hasData && slot.desc.getFields(false).length > 0) { + throw new InvalidClassException("incompatible class change to value class: " + + "stream class has non-empty super type: " + desc.getName()); + } + } + // Read values for the value class fields + FieldValues fieldValues = new FieldValues(desc, true); + + // Get value object constructor adapted to take primitive value buffer and object array. + MethodHandle consMH = ConstructorSupport.deserializationValueCons(desc); + try { + Object obj = (Object) consMH.invokeExact(fieldValues.primValues, fieldValues.objValues); + if (!unshared) + handles.setObject(passHandle, obj); + return obj; + } catch (Exception e) { + throw new InvalidObjectException(e.getMessage(), e); + } catch (Error e) { + throw e; + } catch (Throwable t) { + throw new InvalidObjectException("ReflectiveOperationException " + + "during deserialization", t); + } } /** - * If obj is non-null, reads externalizable data by invoking readExternal() + * Creates a new object and invokes its readExternal method to read its contents. + * + * If the class is instantiable, read externalizable data by invoking readExternal() * method of obj; otherwise, attempts to skip over externalizable data. * Expects that passHandle is set to obj's handle before this method is - * called. + * called. The new object is entered in the handle table immediately, + * allowing it to leak before it is completely read. */ - private void readExternalData(Externalizable obj, ObjectStreamClass desc) + private Object readExternalObject(ObjectStreamClass desc, boolean unshared) throws IOException { + TRACE("readExternalObject: %s", desc.getName()); + + // For Externalizable objects, + // create the instance, publish the ref, and read the data + Externalizable obj = null; + try { + if (desc.isInstantiable()) { + obj = (Externalizable) desc.newInstance(); + } + } catch (Exception ex) { + throw new InvalidClassException(desc.getName(), + "unable to create instance", ex); + } + + if (!unshared) + handles.setObject(passHandle, obj); + SerialCallbackContext oldContext = curContext; if (oldContext != null) oldContext.check(); @@ -2383,6 +2450,7 @@ private void readExternalData(Externalizable obj, ObjectStreamClass desc) * externalizable data remains in the stream, a subsequent read will * most likely throw a StreamCorruptedException. */ + return obj; } /** @@ -2391,14 +2459,15 @@ private void readExternalData(Externalizable obj, ObjectStreamClass desc) * mechanism marks the record as having an exception. * Null is returned from readRecord and later the exception is thrown at * the exit of {@link #readObject(Class)}. - **/ - private Object readRecord(ObjectStreamClass desc) throws IOException { - ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); - if (slots.length != 1) { + */ + private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOException { + TRACE("invoking readRecord: %s", desc.getName()); + List slots = desc.getClassDataLayout(); + if (slots.size() != 1) { // skip any superclass stream field values - for (int i = 0; i < slots.length-1; i++) { - if (slots[i].hasData) { - new FieldValues(slots[i].desc, true); + for (int i = 0; i < slots.size()-1; i++) { + if (slots.get(i).hasData) { + new FieldValues(slots.get(i).desc, true); } } } @@ -2412,10 +2481,13 @@ private Object readRecord(ObjectStreamClass desc) throws IOException { // - byte[] primValues // - Object[] objValues // and return Object - MethodHandle ctrMH = RecordSupport.deserializationCtr(desc); + MethodHandle ctrMH = ConstructorSupport.deserializationCtr(desc); try { - return (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues); + Object obj = (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues); + if (!unshared) + handles.setObject(passHandle, obj); + return obj; } catch (Exception e) { throw new InvalidObjectException(e.getMessage(), e); } catch (Error e) { @@ -2427,114 +2499,207 @@ private Object readRecord(ObjectStreamClass desc) throws IOException { } /** - * Reads (or attempts to skip, if obj is null or is tagged with a - * ClassNotFoundException) instance data for each serializable class of - * object in stream, from superclass to subclass. Expects that passHandle - * is set to obj's handle before this method is called. + * Construct an object from the stream for a class that has only default read object behaviors. + * For each object, the fields are read before any are assigned. + * The new instance is entered in the handle table if it is unshared, + * allowing it to escape before it is initialized. + * The `readObject` and `readObjectNoData` methods are not present and are not called. + * + * @param desc the class descriptor + * @param unshared true if the object should be shared + * @return the object constructed from the stream data + * @throws IOException if there are I/O errors while reading from the + * underlying {@code InputStream} + * @throws InvalidClassException if the instance creation fails */ - private void readSerialData(Object obj, ObjectStreamClass desc) - throws IOException - { - ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); - // Best effort Failure Atomicity; slotValues will be non-null if field - // values can be set after reading all field data in the hierarchy. - // Field values can only be set after reading all data if there are no - // user observable methods in the hierarchy, readObject(NoData). The - // top most Serializable class in the hierarchy can be skipped. - FieldValues[] slotValues = null; - - boolean hasSpecialReadMethod = false; - for (int i = 1; i < slots.length; i++) { - ObjectStreamClass slotDesc = slots[i].desc; - if (slotDesc.hasReadObjectMethod() - || slotDesc.hasReadObjectNoDataMethod()) { - hasSpecialReadMethod = true; - break; + private Object readSerialDefaultObject(ObjectStreamClass desc, boolean unshared) + throws IOException, InvalidClassException { + if (!desc.isInstantiable()) { + // No local class to create, read and discard + return readAbsentLocalClass(desc, unshared); + } + TRACE("readSerialDefaultObject: %s", desc.getName()); + try { + final Object obj = desc.newInstance(); + if (!unshared) + handles.setObject(passHandle, obj); + + // Best effort Failure Atomicity; slotValues will be non-null if field + // values can be set after reading all field data in the hierarchy. + List slotValues = desc.getClassDataLayout().stream() + .filter(s -> s.hasData) + .map(s1 -> { + var values = new FieldValues(s1.desc, true); + finishBlockData(s1.desc); + return values; + }) + .toList(); + + if (handles.lookupException(passHandle) != null) { + return null; // some exception for a class, do not return the object } + + // Check that the types are assignable for all slots before assigning. + slotValues.forEach(v -> v.defaultCheckFieldValues(obj)); + slotValues.forEach(v -> v.defaultSetFieldValues(obj)); + return obj; + } catch (InstantiationException | InvocationTargetException ex) { + throw new InvalidClassException(desc.forClass().getName(), + "unable to create instance", ex); } - // No special read methods, can store values and defer setting. - if (!hasSpecialReadMethod) - slotValues = new FieldValues[slots.length]; + } - for (int i = 0; i < slots.length; i++) { - ObjectStreamClass slotDesc = slots[i].desc; - if (slots[i].hasData) { - if (obj == null || handles.lookupException(passHandle) != null) { - // Read fields of the current descriptor into a new FieldValues and discard - new FieldValues(slotDesc, true); - } else if (slotDesc.hasReadObjectMethod()) { - SerialCallbackContext oldContext = curContext; - if (oldContext != null) - oldContext.check(); - try { - curContext = new SerialCallbackContext(obj, slotDesc); + /** + * Reads (or attempts to skip, if not instantiatable or is tagged with a + * ClassNotFoundException) instance data for each serializable class of + * object in stream, from superclass to subclass. + * Expects that passHandle is set to current handle before this method is called. + */ + private Object readSerialCustomData(ObjectStreamClass desc, boolean unshared) + throws IOException + { + if (!desc.isInstantiable()) { + // No local class to create, read and discard + return readAbsentLocalClass(desc, unshared); + } - bin.setBlockDataMode(true); - slotDesc.invokeReadObject(obj, this); - } catch (ClassNotFoundException ex) { - /* - * In most cases, the handle table has already - * propagated a CNFException to passHandle at this - * point; this mark call is included to address cases - * where the custom readObject method has cons'ed and - * thrown a new CNFException of its own. - */ - handles.markException(passHandle, ex); - } finally { - curContext.setUsed(); - if (oldContext!= null) - oldContext.check(); - curContext = oldContext; - } + TRACE("readSerialCustomData: %s, ex: %s", desc.getName(), handles.lookupException(passHandle)); + try { + Object obj = desc.newInstance(); + if (!unshared) + handles.setObject(passHandle, obj); + // Read data into each of the slots for the class + return readSerialCustomSlots(obj, desc.getClassDataLayout()); + } catch (InstantiationException | InvocationTargetException ex) { + throw new InvalidClassException(desc.forClass().getName(), + "unable to create instance", ex); + } + } - /* - * defaultDataEnd may have been set indirectly by custom - * readObject() method when calling defaultReadObject() or - * readFields(); clear it to restore normal read behavior. - */ - defaultDataEnd = false; + /** + * Reads from the stream using custom or default readObject methods appropriate. + * For each slot, either the custom readObject method or the default reader of fields + * is invoked. Unused slot specific custom data is discarded. + * This function is used by {@link #readSerialCustomData}. + * + * @param obj the object to assign the values to + * @param slots a list of slots to read from the stream + * @return the object being initialized + * @throws IOException if there are I/O errors while reading from the + * underlying {@code InputStream} + */ + private Object readSerialCustomSlots(Object obj, List slots) throws IOException { + TRACE(" readSerialCustomSlots: %s", slots); + + for (ClassDataSlot slot : slots) { + ObjectStreamClass slotDesc = slot.desc; + if (slot.hasData) { + if (slotDesc.hasReadObjectMethod() && + handles.lookupException(passHandle) == null) { + // Invoke slot custom readObject method + readSlotViaReadObject(obj, slotDesc); } else { // Read fields of the current descriptor into a new FieldValues FieldValues values = new FieldValues(slotDesc, true); - if (slotValues != null) { - slotValues[i] = values; - } else if (obj != null) { - if (handles.lookupException(passHandle) == null) { - // passHandle NOT marked with an exception; set field values - values.defaultCheckFieldValues(obj); - values.defaultSetFieldValues(obj); - } + if (handles.lookupException(passHandle) == null) { + // Set the instance fields if no previous exception + values.defaultCheckFieldValues(obj); + values.defaultSetFieldValues(obj); } - } - - if (slotDesc.hasWriteObjectData()) { - skipCustomData(); - } else { - bin.setBlockDataMode(false); + finishBlockData(slotDesc); } } else { - if (obj != null && - slotDesc.hasReadObjectNoDataMethod() && - handles.lookupException(passHandle) == null) - { + if (slotDesc.hasReadObjectNoDataMethod() && + handles.lookupException(passHandle) == null) { slotDesc.invokeReadObjectNoData(obj); } } } + return obj; + } - if (obj != null && slotValues != null && handles.lookupException(passHandle) == null) { - // passHandle NOT marked with an exception - // Check that the non-primitive types are assignable for all slots - // before assigning. - for (int i = 0; i < slots.length; i++) { - if (slotValues[i] != null) - slotValues[i].defaultCheckFieldValues(obj); - } - for (int i = 0; i < slots.length; i++) { - if (slotValues[i] != null) - slotValues[i].defaultSetFieldValues(obj); + /** + * Invoke the readObject method of the class to read and store the state from the stream. + * + * @param obj an instance of the class being created, only partially initialized. + * @param slotDesc the ObjectStreamDescriptor for the current class + * @throws IOException if there are I/O errors while reading from the + * underlying {@code InputStream} + */ + private void readSlotViaReadObject(Object obj, ObjectStreamClass slotDesc) throws IOException { + TRACE("readSlotViaReadObject: %s", slotDesc.getName()); + assert obj != null : "readSlotViaReadObject called when obj == null"; + + SerialCallbackContext oldContext = curContext; + if (oldContext != null) + oldContext.check(); + try { + curContext = new SerialCallbackContext(obj, slotDesc); + + bin.setBlockDataMode(true); + slotDesc.invokeReadObject(obj, this); + } catch (ClassNotFoundException ex) { + /* + * In most cases, the handle table has already + * propagated a CNFException to passHandle at this + * point; this mark call is included to address cases + * where the custom readObject method has cons'ed and + * thrown a new CNFException of its own. + */ + handles.markException(passHandle, ex); + } finally { + curContext.setUsed(); + if (oldContext!= null) + oldContext.check(); + curContext = oldContext; + } + + /* + * defaultDataEnd may have been set indirectly by custom + * readObject() method when calling defaultReadObject() or + * readFields(); clear it to restore normal read behavior. + */ + defaultDataEnd = false; + + finishBlockData(slotDesc); + } + + + /** + * Read and discard an entire object, leaving a null reference in the HandleTable. + * The descriptor of the class in the stream is used to read the fields from the stream. + * There is no instance in which to store the field values. + * Custom data following the fields of any slot is read and discarded. + * References to nested objects are read and retained in the + * handle table using the regular mechanism. + * Handles later in the stream may refer to the nested objects. + * + * @param desc the stream class descriptor + * @param unshared the unshared flag, ignored since no object is created + * @return null, no object is created + * @throws IOException if there are I/O errors while reading from the + * underlying {@code InputStream} + */ + private Object readAbsentLocalClass(ObjectStreamClass desc, boolean unshared) + throws IOException { + TRACE("readAbsentLocalClass: %s", desc.getName()); + desc.getClassDataLayout().stream() + .filter(s -> s.hasData) + .forEach(s2 -> {new FieldValues(s2.desc, true); finishBlockData(s2.desc);}); + return null; + } + + // Finish handling of block data by skipping any remaining and setting BlockDataMode = false + private void finishBlockData(ObjectStreamClass slotDesc) throws UncheckedIOException { + try { + if (slotDesc.hasWriteObjectData()) { + skipCustomData(); + } else { + bin.setBlockDataMode(false); } + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); } } @@ -2630,32 +2795,38 @@ private final class FieldValues extends GetField { * @param desc the ObjectStreamClass to read * @param recordDependencies if true, record the dependencies * from current PassHandle and the object's read. + * @throws UncheckedIOException if any IOException occurs */ - FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws IOException { - this.desc = desc; + FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws UncheckedIOException { + try { + this.desc = desc; + TRACE(" reading FieldValues: %s", desc.getName()); + int primDataSize = desc.getPrimDataSize(); + primValues = (primDataSize > 0) ? new byte[primDataSize] : null; + if (primDataSize > 0) { + bin.readFully(primValues, 0, primDataSize, false); + } - int primDataSize = desc.getPrimDataSize(); - primValues = (primDataSize > 0) ? new byte[primDataSize] : null; - if (primDataSize > 0) { - bin.readFully(primValues, 0, primDataSize, false); - } - int numObjFields = desc.getNumObjFields(); - objValues = (numObjFields > 0) ? new Object[numObjFields] : null; - objHandles = (numObjFields > 0) ? new int[numObjFields] : null; - if (numObjFields > 0) { - int objHandle = passHandle; - ObjectStreamField[] fields = desc.getFields(false); - int numPrimFields = fields.length - objValues.length; - for (int i = 0; i < objValues.length; i++) { - ObjectStreamField f = fields[numPrimFields + i]; - objValues[i] = readObject0(Object.class, f.isUnshared()); - objHandles[i] = passHandle; - if (recordDependencies && f.getField() != null) { - handles.markDependency(objHandle, passHandle); + int numObjFields = desc.getNumObjFields(); + objValues = (numObjFields > 0) ? new Object[numObjFields] : null; + objHandles = (numObjFields > 0) ? new int[numObjFields] : null; + if (numObjFields > 0) { + int objHandle = passHandle; + ObjectStreamField[] fields = desc.getFields(false); + int numPrimFields = fields.length - objValues.length; + for (int i = 0; i < objValues.length; i++) { + ObjectStreamField f = fields[numPrimFields + i]; + objValues[i] = readObject0(Object.class, f.isUnshared()); + objHandles[i] = passHandle; + if (recordDependencies && f.getField() != null) { + handles.markDependency(objHandle, passHandle); + } } + passHandle = objHandle; } - passHandle = objHandle; + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); } } diff --git a/src/java.base/share/classes/java/io/ObjectOutputStream.java b/src/java.base/share/classes/java/io/ObjectOutputStream.java index 6ce6a7a3694..65041a305a4 100644 --- a/src/java.base/share/classes/java/io/ObjectOutputStream.java +++ b/src/java.base/share/classes/java/io/ObjectOutputStream.java @@ -36,6 +36,8 @@ import jdk.internal.util.ByteArray; import sun.reflect.misc.ReflectUtil; +import static java.io.ObjectInputStream.TRACE; + /** * An ObjectOutputStream writes primitive data types and graphs of Java objects * to an OutputStream. The objects can be read (reconstituted) using an @@ -158,9 +160,36 @@ * defaultWriteObject and writeFields initially terminate any existing * block-data record. * + * *

    Records are serialized differently than ordinary serializable or externalizable * objects, see record serialization. * + * + *

    Value classes are {@linkplain Serializable} through the use of the serialization proxy pattern. + * The serialization protocol does not support a standard serialized form for value classes. + * The value class delegates to a serialization proxy by supplying an alternate + * record or object to be serialized instead of the value class. + * When the proxy is deserialized it re-constructs the value object and returns the value object. + * For example, + * {@snippet lang="java" : + * value class ZipCode implements Serializable { // @highlight substring="value class" + * private static final long serialVersionUID = 1L; + * private int zipCode; + * public ZipCode(int zip) { this.zipCode = zip; } + * public int zipCode() { return zipCode; } + * + * public Object writeReplace() { // @highlight substring="writeReplace" + * return new ZipCodeProxy(zipCode); + * } + * + * private record ZipCodeProxy(int zipCode) implements Serializable { + * public Object readResolve() { // @highlight substring="readResolve" + * return new ZipCode(zipCode); + * } + * } + * } + * } + * * @spec serialization/index.html Java Object Serialization Specification * @author Mike Warres * @author Roger Riggs @@ -344,6 +373,9 @@ public void useProtocolVersion(int version) throws IOException { * object are written transitively so that a complete equivalent graph of * objects can be reconstructed by an ObjectInputStream. * + *

    Serialization and deserialization of value classes is described in + * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}. + * *

    Exceptions are thrown for problems with the OutputStream and for * classes that should not be serialized. All exceptions are fatal to the * OutputStream, which is left in an indeterminate state, and it is up to @@ -413,6 +445,9 @@ protected void writeObjectOverride(Object obj) throws IOException { * writeUnshared, and not to any transitively referenced sub-objects in the * object graph to be serialized. * + *

    Serialization and deserialization of value classes is described in + * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}. + * *

    ObjectOutputStream subclasses which override this method can only be * constructed in security contexts possessing the * "enableSubclassImplementation" SerializablePermission; any attempt to @@ -1198,9 +1233,6 @@ private void writeObject0(Object obj, boolean unshared) } else if (obj instanceof Enum) { writeEnum((Enum) obj, desc, unshared); } else if (obj instanceof Serializable) { - if (cl.isValue() && !desc.isInstantiable()) { - throw new NotSerializableException(cl.getName()); - } writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { @@ -1456,8 +1488,8 @@ private void writeOrdinaryObject(Object obj, if (desc.isRecord()) { writeRecordData(obj, desc); } else if (desc.isExternalizable() && !desc.isProxy()) { - if (desc.forClass().isValue()) - throw new NotSerializableException("Externalizable not valid for value class " + if (desc.isValue()) + throw new InvalidClassException("Externalizable not valid for value class " + desc.forClass().getName()); writeExternalData((Externalizable) obj); } else { @@ -1507,10 +1539,10 @@ private void writeRecordData(Object obj, ObjectStreamClass desc) throws IOException { assert obj.getClass().isRecord(); - ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); - if (slots.length != 1) { + List slots = desc.getClassDataLayout(); + if (slots.size() != 1) { throw new InvalidClassException( - "expected a single record slot length, but found: " + slots.length); + "expected a single record slot length, but found: " + slots.size()); } defaultWriteFields(obj, desc); // #### seems unnecessary to use the accessors @@ -1523,9 +1555,9 @@ private void writeRecordData(Object obj, ObjectStreamClass desc) private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { - ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); - for (int i = 0; i < slots.length; i++) { - ObjectStreamClass slotDesc = slots[i].desc; + List slots = desc.getClassDataLayout(); + for (int i = 0; i < slots.size(); i++) { + ObjectStreamClass slotDesc = slots.get(i).desc; if (slotDesc.hasWriteObjectMethod()) { PutFieldImpl oldPut = curPut; curPut = null; diff --git a/src/java.base/share/classes/java/io/ObjectStreamClass.java b/src/java.base/share/classes/java/io/ObjectStreamClass.java index ef84e36864c..05f045e5e6a 100644 --- a/src/java.base/share/classes/java/io/ObjectStreamClass.java +++ b/src/java.base/share/classes/java/io/ObjectStreamClass.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.AccessFlag; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InaccessibleObjectException; @@ -54,9 +53,11 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import jdk.internal.MigratedValueClass; import jdk.internal.event.SerializationMisdeclarationEvent; @@ -67,8 +68,11 @@ import jdk.internal.access.SharedSecrets; import jdk.internal.access.JavaSecurityAccess; import jdk.internal.util.ByteArray; +import jdk.internal.value.DeserializeConstructor; import sun.reflect.misc.ReflectUtil; +import static java.io.ObjectInputStream.TRACE; + /** * Serialization's descriptor for classes. It contains the name and * serialVersionUID of the class. The ObjectStreamClass for a specific class @@ -107,6 +111,71 @@ public final class ObjectStreamClass implements Serializable { AccessController.doPrivileged( new ReflectionFactory.GetReflectionFactoryAction()); + /** + * The mode of deserialization for a class depending on its type and interfaces. + * The markers used are {@linkplain java.io.Serializable}, {@linkplain java.io.Externalizable}, + * Class.isRecord(), Class.isValue(), constructors, and + * the presence of methods `readObject`, `writeObject`, `readObjectNoData`, `writeObject`. + * ObjectInputStream dispatches on the mode to construct objects from the stream. + */ + enum DeserializationMode { + /** + * Construct an object from the stream for a class that has only default read object behaviors. + * All classes and superclasses use defaultReadObject; no custom readObject or readObjectNoData. + * The new instance is entered in the handle table if it is unshared, + * allowing it to escape before it is initialized. + * For each object, all the fields are read before any are assigned. + * The `readObject` and `readObjectNoData` methods are not present and are not called. + */ + READ_OBJECT_DEFAULT, + /** + * Creates a new object and invokes its readExternal method to read its contents. + * If the class is instantiable, read externalizable data by invoking readExternal() + * method of obj; otherwise, attempts to skip over externalizable data. + * Expects that passHandle is set to obj's handle before this method is + * called. The new object is entered in the handle table immediately, + * allowing it to leak before it is completely read. + */ + READ_EXTERNALIZABLE, + /** + * Read all the record fields and invoke its canonical constructor. + * Construct the record using its canonical constructor. + * The new record is entered in the handle table only after the constructor returns. + */ + READ_RECORD, + /** + * Fully custom read from the stream to create an instance. + * If the class is not instantiatable or is tagged with ClassNotFoundException + * the data in the stream for the class is read and discarded. {@link #READ_NO_LOCAL_CLASS} + * The instance is created and set in the handle table, allowing it to leak before it is initialized. + * For each serializable class in the stream, from superclass to subclass the + * stream values are read by the `readObject` method, if present, or defaultReadObject. + * Custom inline data is discarded if not consumed by the class `readObject` method. + */ + READ_OBJECT_CUSTOM, + /** + * Construct an object by reading the values of all fields and + * invoking a constructor or static factory method. + * The constructor or static factory method is selected by matching its parameters with the + * sequence of field types of the serializable fields of the local class and superclasses. + * Invoke the constructor with all the values from the stream, inserting + * defaults and dropping extra values as necessary. + * This is very similar to the reading of records, except for the identification of + * the constructor or static factory. + */ + READ_OBJECT_VALUE, + /** + * Read and discard an entire object, leaving a null reference in the HandleTable. + * The descriptor of the class in the stream is used to read the fields from the stream. + * There is no instance in which to store the field values. + * Custom data following the fields of any slot is read and discarded. + * References to nested objects are read and retained in the + * handle table using the regular mechanism. + * Handles later in the stream may refer to the nested objects. + */ + READ_NO_LOCAL_CLASS, + } + private static class Caches { /** cache mapping local classes -> descriptors */ static final ClassCache localDescs = @@ -142,6 +211,8 @@ protected Map computeValue(Class type) { private boolean isRecord; /** true if represents a value class */ private boolean isValue; + /** The DeserializationMode for this class. */ + private DeserializationMode factoryMode; /** true if represented class implements Serializable */ private boolean serializable; /** true if represented class implements Externalizable */ @@ -199,7 +270,7 @@ InvalidClassException newInvalidClassException() { /** reflector for setting/getting serializable field values */ private FieldReflector fieldRefl; /** data layout of serialized objects described by this class desc */ - private volatile ClassDataSlot[] dataLayout; + private volatile List dataLayout; /** serialization-appropriate constructor, or null if none */ private Constructor cons; @@ -413,13 +484,34 @@ public Void run() { } if (isRecord) { + factoryMode = DeserializationMode.READ_RECORD; canonicalCtr = canonicalRecordCtr(cl); deserializationCtrs = new DeserializationConstructorsCache(); - } else if (isValue) { - // Value object instance creation is specialized in newInstance() - cons = null; } else if (externalizable) { - cons = getExternalizableConstructor(cl); + factoryMode = DeserializationMode.READ_EXTERNALIZABLE; + if (cl.isIdentity()) { + cons = getExternalizableConstructor(cl); + } else { + serializeEx = deserializeEx = new ExceptionInfo(cl.getName(), + "Externalizable not valid for value class"); + } + } else if (cl.isValue()) { + factoryMode = DeserializationMode.READ_OBJECT_VALUE; + if (!cl.isAnnotationPresent(MigratedValueClass.class)) { + serializeEx = deserializeEx = new ExceptionInfo(cl.getName(), + "Value class serialization is only supported with `writeReplace`"); + } else if (Modifier.isAbstract(cl.getModifiers())) { + serializeEx = deserializeEx = new ExceptionInfo(cl.getName(), + "value class is abstract"); + } else { + // Value classes should have constructor(s) annotated with {@link DeserializeConstructor} + canonicalCtr = getDeserializingValueCons(cl, fields); + deserializationCtrs = new DeserializationConstructorsCache(); factoryMode = DeserializationMode.READ_OBJECT_VALUE; + if (canonicalCtr == null) { + serializeEx = deserializeEx = new ExceptionInfo(cl.getName(), + "no constructor or factory found for migrated value class"); + } + } } else { cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject", @@ -431,6 +523,10 @@ public Void run() { readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData", null, Void.TYPE); hasWriteObjectData = (writeObjectMethod != null); + factoryMode = ((superDesc == null || superDesc.factoryMode() == DeserializationMode.READ_OBJECT_DEFAULT) + && readObjectMethod == null && readObjectNoDataMethod == null) + ? DeserializationMode.READ_OBJECT_DEFAULT + : DeserializationMode.READ_OBJECT_CUSTOM; } domains = getProtectionDomains(cons, cl); writeReplaceMethod = getInheritableMethod( @@ -575,6 +671,9 @@ void initProxy(Class cl, deserializeEx = localDesc.deserializeEx; domains = localDesc.domains; cons = localDesc.cons; + factoryMode = localDesc.factoryMode; + } else { + factoryMode = DeserializationMode.READ_OBJECT_DEFAULT; } fieldRefl = getReflector(fields, localDesc); initialized = true; @@ -669,6 +768,12 @@ void initNonProxy(ObjectStreamClass model, domains = localDesc.domains; assert cl.isRecord() ? localDesc.cons == null : true; cons = localDesc.cons; + factoryMode = localDesc.factoryMode; + } else { + // No local class, read data using only the schema from the stream + factoryMode = (externalizable) + ? DeserializationMode.READ_EXTERNALIZABLE + : DeserializationMode.READ_NO_LOCAL_CLASS; } fieldRefl = getReflector(fields, localDesc); @@ -724,7 +829,7 @@ void readNonProxy(ObjectInputStream in) String signature = ((tcode == 'L') || (tcode == '[')) ? in.readTypeString() : String.valueOf(tcode); try { - fields[i] = new ObjectStreamField(fname, signature, false); + fields[i] = new ObjectStreamField(fname, signature, false, -1); } catch (RuntimeException e) { throw new InvalidClassException(name, "invalid descriptor for field " + @@ -943,6 +1048,14 @@ boolean isValue() { return isValue; } + /** + * {@return the factory mode for deserialization} + */ + DeserializationMode factoryMode() { + requireInitialized(); + return factoryMode; + } + /** * Returns true if class descriptor represents externalizable class that * has written its data in 1.2 (block data) format, false otherwise. @@ -967,13 +1080,13 @@ boolean hasWriteObjectData() { * be instantiated by the serialization runtime--i.e., if it is * externalizable and defines a public no-arg constructor, if it is * non-externalizable and its first non-serializable superclass defines an - * accessible no-arg constructor, or if the class is a migrated value class. + * accessible no-arg constructor, or if the class is a value class with a @DeserializeConstructor + * constructor or static factory. * Otherwise, returns false. */ boolean isInstantiable() { requireInitialized(); - return (cons != null | - (isValue && cl != null && cl.isAnnotationPresent(jdk.internal.MigratedValueClass.class))); + return (cons != null || (isValue() && canonicalCtr != null)); } /** @@ -1084,23 +1197,11 @@ Object newInstance() ex.initCause(err); throw ex; } - } else if (isValue) { - // Start with a buffered default value. - return FieldReflector.newValueInstance(cl); - } else { + } else { throw new UnsupportedOperationException(); } } - /** - * Finish the initialization of a value object. - * @param obj an object (larval if a value object) - * @return the finished object - */ - Object finishValue(Object obj) { - return (isValue) ? FieldReflector.finishValueInstance(obj) : obj; - } - /** * Invokes the writeObject method of the represented serializable class. * Throws UnsupportedOperationException if this class descriptor is not @@ -1271,23 +1372,18 @@ static class ClassDataSlot { } /** - * Returns array of ClassDataSlot instances representing the data layout + * Returns a List of ClassDataSlot instances representing the data layout * (including superclass data) for serialized objects described by this * class descriptor. ClassDataSlots are ordered by inheritance with those * containing "higher" superclasses appearing first. The final * ClassDataSlot contains a reference to this descriptor. */ - ClassDataSlot[] getClassDataLayout() throws InvalidClassException { + List getClassDataLayout() throws InvalidClassException { // REMIND: synchronize instead of relying on volatile? - if (dataLayout == null) { - dataLayout = getClassDataLayout0(); - } - return dataLayout; - } + List layout = dataLayout; + if (layout != null) + return layout; - private ClassDataSlot[] getClassDataLayout0() - throws InvalidClassException - { ArrayList slots = new ArrayList<>(); Class start = cl, end = cl; @@ -1336,7 +1432,8 @@ private ClassDataSlot[] getClassDataLayout0() // order slots from superclass -> subclass Collections.reverse(slots); - return slots.toArray(new ClassDataSlot[slots.size()]); + dataLayout = slots; + return slots; } /** @@ -1464,6 +1561,66 @@ private ObjectStreamClass getVariantFor(Class cl) return desc; } + /** + * Return a method handle for the static method or constructor(s) that matches the + * serializable fields and annotated with {@link DeserializeConstructor}. + * The descriptor for the class is still being initialized, so is passed the fields needed. + * @param clazz The class to query + * @param fields the serializable fields of the class + * @return a MethodHandle, null if none found + */ + @SuppressWarnings("unchecked") + private static MethodHandle getDeserializingValueCons(Class clazz, + ObjectStreamField[] fields) { + // Search for annotated static factory in methods or constructors + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle mh = Stream.concat( + Arrays.stream(clazz.getDeclaredMethods()).filter(m -> Modifier.isStatic(m.getModifiers())), + Arrays.stream(clazz.getDeclaredConstructors())) + .filter(m -> m.isAnnotationPresent(DeserializeConstructor.class)) + .map(m -> { + try { + m.setAccessible(true); + return (m instanceof Constructor cons) + ? lookup.unreflectConstructor(cons) + : lookup.unreflect(((Method) m)); + } catch (IllegalAccessException iae) { + throw new InternalError(iae); // should not occur after setAccessible + }}) + .filter(m -> matchFactoryParamTypes(clazz, m, fields)) + .findFirst().orElse(null); + TRACE("DeserializeConstructor for %s, mh: %s", clazz, mh); + return mh; + } + + /** + * Check that the parameters of the factory method match the fields of this class. + * + * @param mh a MethodHandle for a constructor or factory + * @return true if all fields match the parameters, false if not + */ + private static boolean matchFactoryParamTypes(Class clazz, + MethodHandle mh, + ObjectStreamField[] fields) { + TRACE(" matchFactoryParams checking class: %s, mh: %s", clazz, mh); + var params = mh.type().parameterList(); + if (params.size() != fields.length) { + TRACE(" matchFactoryParams %s, arg count mismatch %d params != %d fields", + clazz, params.size(), fields.length); + return false; // Mismatch in count of fields and parameters + } + for (ObjectStreamField field : fields) { + int argIndex = field.getArgIndex(); + final Class paramtype = params.get(argIndex); + if (!field.getType().equals(paramtype)) { + TRACE(" matchFactoryParams %s: argIndex: %d type mismatch field: %s != param: %s", + clazz, argIndex, field.getType(), paramtype); + return false; + } + } + return true; + } + /** * Returns public no-arg constructor of given class, or null if none found. * Access checks are disabled on the returned constructor (if any), since @@ -1710,14 +1867,12 @@ private static ObjectStreamField[] getDeclaredSerialFields(Class cl) if ((f.getType() == spf.getType()) && ((f.getModifiers() & Modifier.STATIC) == 0)) { - boundFields[i] = - new ObjectStreamField(f, spf.isUnshared(), true); + boundFields[i] = new ObjectStreamField(f, spf.isUnshared(), true, i); } } catch (NoSuchFieldException ex) { } if (boundFields[i] == null) { - boundFields[i] = new ObjectStreamField( - fname, spf.getType(), spf.isUnshared()); + boundFields[i] = new ObjectStreamField(fname, spf.getType(), spf.isUnshared(), i); } } return boundFields; @@ -1734,9 +1889,9 @@ private static ObjectStreamField[] getDefaultSerialFields(Class cl) { ArrayList list = new ArrayList<>(); int mask = Modifier.STATIC | Modifier.TRANSIENT; - for (int i = 0; i < clFields.length; i++) { + for (int i = 0, argIndex = 0; i < clFields.length; i++) { if ((clFields[i].getModifiers() & mask) == 0) { - list.add(new ObjectStreamField(clFields[i], false, true)); + list.add(new ObjectStreamField(clFields[i], false, true, argIndex++)); } } int size = list.size(); @@ -1968,33 +2123,6 @@ private static final class FieldReflector { /** field types */ private final Class[] types; - /** - * Return a new instance of the class using Unsafe.uninitializedDefaultValue - * and buffer it. - * @param clazz The value class - * @return a buffered default value - */ - static Object newValueInstance(Class clazz) throws InstantiationException{ - var accessFlags = clazz.accessFlags(); - if (accessFlags.contains(AccessFlag.ABSTRACT) || - accessFlags.contains(AccessFlag.IDENTITY)) { - throw new InstantiationException("Value class not instantiable: " + clazz.getName()); - } - // may not be implicitly constructible; so allocate with Unsafe - Object obj = UNSAFE.uninitializedDefaultValue(clazz); - return UNSAFE.makePrivateBuffer(obj); - } - - /** - * Finish a value object, clear the larval state and returning the value object. - * @param obj a buffered value object in a larval state - * @return the finished value object - */ - static Object finishValueInstance(Object obj) { - assert (obj.getClass().isValue()) : "Should be a value class"; - return UNSAFE.finishPrivateBuffer(obj); - } - /** * Constructs FieldReflector capable of setting/getting values from the * subset of fields whose ObjectStreamFields contain non-null @@ -2147,7 +2275,7 @@ void setObjFieldValues(Object obj, Object[] vals) { } private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) { - if (obj == null) { + if (obj == null && !dryRun) { throw new NullPointerException(); } for (int i = numPrimFields; i < fields.length; i++) { @@ -2288,16 +2416,16 @@ private static ObjectStreamField[] matchFields(ObjectStreamField[] fields, } if (lf.getField() != null) { m = new ObjectStreamField( - lf.getField(), lf.isUnshared(), false); + lf.getField(), lf.isUnshared(), true, lf.getArgIndex()); // Don't hide type } else { m = new ObjectStreamField( - lf.getName(), lf.getSignature(), lf.isUnshared()); + lf.getName(), lf.getSignature(), lf.isUnshared(), lf.getArgIndex()); } } } if (m == null) { m = new ObjectStreamField( - f.getName(), f.getSignature(), false); + f.getName(), f.getSignature(), false, -1); } m.setOffset(f.getOffset()); matches[i] = m; @@ -2419,7 +2547,7 @@ static final class Impl extends Key { } /** Record specific support for retrieving and binding stream field values. */ - static final class RecordSupport { + static final class ConstructorSupport { /** * Returns canonical record constructor adapted to take two arguments: * {@code (byte[] primValues, Object[] objValues)} @@ -2476,6 +2604,65 @@ static MethodHandle deserializationCtr(ObjectStreamClass desc) { desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh); } + /** + * Returns value object constructor adapted to take two arguments: + * {@code (byte[] primValues, Object[] objValues)} and return {@code Object} + */ + static MethodHandle deserializationValueCons(ObjectStreamClass desc) { + // check the cached value 1st + MethodHandle mh = desc.deserializationCtr; + if (mh != null) return mh; + mh = desc.deserializationCtrs.get(desc.getFields(false)); + if (mh != null) return desc.deserializationCtr = mh; + + // retrieve the selected constructor + // (T1, T2, ..., Tn):TR + ObjectStreamClass localDesc = desc.localDesc; + mh = localDesc.canonicalCtr; + MethodType mt = mh.type(); + + // change return type to Object + // (T1, T2, ..., Tn):TR -> (T1, T2, ..., Tn):Object + mh = mh.asType(mh.type().changeReturnType(Object.class)); + + // drop last 2 arguments representing primValues and objValues arrays + // (T1, T2, ..., Tn):Object -> (T1, T2, ..., Tn, byte[], Object[]):Object + mh = MethodHandles.dropArguments(mh, mh.type().parameterCount(), byte[].class, Object[].class); + + Class[] params = mt.parameterArray(); + for (int i = params.length-1; i >= 0; i--) { + // Get the name from the local descriptor matching the argIndex + var field = getFieldForArgIndex(localDesc, i); + String name = (field == null) ? "" : field.getName(); // empty string to supply default + Class type = params[i]; + // obtain stream field extractor that extracts argument at + // position i (Ti+1) from primValues and objValues arrays + // (byte[], Object[]):Ti+1 + MethodHandle combiner = streamFieldExtractor(name, type, desc); + // fold byte[] privValues and Object[] objValues into argument at position i (Ti+1) + // (..., Ti, Ti+1, byte[], Object[]):Object -> (..., Ti, byte[], Object[]):Object + mh = MethodHandles.foldArguments(mh, i, combiner); + } + // what we are left with is a MethodHandle taking just the primValues + // and objValues arrays and returning the constructed instance + // (byte[], Object[]):Object + + // store it into cache and return the 1st value stored + return desc.deserializationCtr = + desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh); + } + + // Find the ObjectStreamField for the argument index, otherwise null + private static ObjectStreamField getFieldForArgIndex(ObjectStreamClass desc, int argIndex) { + for (var field : desc.fields) { + if (field.getArgIndex() == argIndex) + return field; + } + TRACE("field for ArgIndex is null: %s, index: %d, fields: %s", + desc, argIndex, Arrays.toString(desc.fields)); + return null; + } + /** Returns the number of primitive fields for the given descriptor. */ private static int numberPrimValues(ObjectStreamClass desc) { ObjectStreamField[] fields = desc.getFields(); diff --git a/src/java.base/share/classes/java/io/ObjectStreamField.java b/src/java.base/share/classes/java/io/ObjectStreamField.java index 75c955440c1..fd9fbc8728c 100644 --- a/src/java.base/share/classes/java/io/ObjectStreamField.java +++ b/src/java.base/share/classes/java/io/ObjectStreamField.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -57,6 +57,8 @@ public class ObjectStreamField private final Field field; /** offset of field value in enclosing field group */ private int offset; + /** index of the field in the class, retain the declaration order of serializable fields */ + private final int argIndex; /** * Create a Serializable field with the specified type. This field should @@ -87,6 +89,11 @@ public ObjectStreamField(String name, Class type) { * @since 1.4 */ public ObjectStreamField(String name, Class type, boolean unshared) { + this(name, type, unshared, -1); + } + + /* package-private */ + ObjectStreamField(String name, Class type, boolean unshared, int argIndex) { if (name == null) { throw new NullPointerException(); } @@ -95,13 +102,14 @@ public ObjectStreamField(String name, Class type, boolean unshared) { this.unshared = unshared; this.field = null; this.signature = null; + this.argIndex = argIndex; } /** * Creates an ObjectStreamField representing a field with the given name, * signature and unshared setting. */ - ObjectStreamField(String name, String signature, boolean unshared) { + ObjectStreamField(String name, String signature, boolean unshared, int argIndex) { if (name == null) { throw new NullPointerException(); } @@ -109,6 +117,7 @@ public ObjectStreamField(String name, Class type, boolean unshared) { this.signature = signature.intern(); this.unshared = unshared; this.field = null; + this.argIndex = argIndex; type = switch (signature.charAt(0)) { case 'Z' -> Boolean.TYPE; @@ -132,13 +141,14 @@ public ObjectStreamField(String name, Class type, boolean unshared) { * ObjectStreamField (if non-primitive) will return Object.class (as * opposed to a more specific reference type). */ - ObjectStreamField(Field field, boolean unshared, boolean showType) { + ObjectStreamField(Field field, boolean unshared, boolean showType, int argIndex) { this.field = field; this.unshared = unshared; name = field.getName(); Class ftype = field.getType(); type = (showType || ftype.isPrimitive()) ? ftype : Object.class; signature = ftype.descriptorString().intern(); + this.argIndex = argIndex; } /** @@ -227,6 +237,13 @@ protected void setOffset(int offset) { this.offset = offset; } + /** + * {@return Index of the field in the sequence of Serializable fields} + */ + int getArgIndex() { + return argIndex; + } + /** * Return true if this field has a primitive type. * diff --git a/src/java.base/share/classes/java/lang/Boolean.java b/src/java.base/share/classes/java/lang/Boolean.java index 27211ec783e..149c704dddf 100644 --- a/src/java.base/share/classes/java/lang/Boolean.java +++ b/src/java.base/share/classes/java/lang/Boolean.java @@ -25,17 +25,14 @@ package java.lang; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.IntrinsicCandidate; import java.lang.constant.Constable; -import java.lang.constant.ConstantDesc; import java.lang.constant.ConstantDescs; import java.lang.constant.DynamicConstantDesc; import java.util.Optional; -import static java.lang.constant.ConstantDescs.BSM_GET_STATIC_FINAL; -import static java.lang.constant.ConstantDescs.CD_Boolean; - /** * The Boolean class wraps a value of the primitive type * {@code boolean} in an object. An object of type @@ -176,6 +173,7 @@ public boolean booleanValue() { * @since 1.4 */ @IntrinsicCandidate + @DeserializeConstructor public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); } diff --git a/src/java.base/share/classes/java/lang/Byte.java b/src/java.base/share/classes/java/lang/Byte.java index f900f534690..d7ae473455b 100644 --- a/src/java.base/share/classes/java/lang/Byte.java +++ b/src/java.base/share/classes/java/lang/Byte.java @@ -26,6 +26,7 @@ package java.lang; import jdk.internal.misc.CDS; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -35,7 +36,6 @@ import static java.lang.constant.ConstantDescs.BSM_EXPLICIT_CAST; import static java.lang.constant.ConstantDescs.CD_byte; -import static java.lang.constant.ConstantDescs.CD_int; import static java.lang.constant.ConstantDescs.DEFAULT_NAME; /** @@ -145,6 +145,7 @@ private ByteCache() {} * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Byte valueOf(byte b) { final int offset = 128; return ByteCache.cache[(int)b + offset]; diff --git a/src/java.base/share/classes/java/lang/Character.java b/src/java.base/share/classes/java/lang/Character.java index d55c30a0b11..b3299f03a5e 100644 --- a/src/java.base/share/classes/java/lang/Character.java +++ b/src/java.base/share/classes/java/lang/Character.java @@ -26,6 +26,7 @@ package java.lang; import jdk.internal.misc.CDS; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -9014,6 +9015,7 @@ private CharacterCache(){} * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Character valueOf(char c) { if (c <= 127) { // must cache return CharacterCache.cache[(int)c]; diff --git a/src/java.base/share/classes/java/lang/Double.java b/src/java.base/share/classes/java/lang/Double.java index 5cd02386d9b..edca1de8c0d 100644 --- a/src/java.base/share/classes/java/lang/Double.java +++ b/src/java.base/share/classes/java/lang/Double.java @@ -33,6 +33,7 @@ import jdk.internal.math.FloatingDecimal; import jdk.internal.math.DoubleConsts; import jdk.internal.math.DoubleToDecimal; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.IntrinsicCandidate; /** @@ -923,6 +924,7 @@ public static Double valueOf(String s) throws NumberFormatException { * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Double valueOf(double d) { return new Double(d); } diff --git a/src/java.base/share/classes/java/lang/Float.java b/src/java.base/share/classes/java/lang/Float.java index 8b1b1be2748..1457b1218a1 100644 --- a/src/java.base/share/classes/java/lang/Float.java +++ b/src/java.base/share/classes/java/lang/Float.java @@ -33,6 +33,7 @@ import jdk.internal.math.FloatConsts; import jdk.internal.math.FloatingDecimal; import jdk.internal.math.FloatToDecimal; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.IntrinsicCandidate; /** @@ -550,6 +551,7 @@ public static Float valueOf(String s) throws NumberFormatException { * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Float valueOf(float f) { return new Float(f); } diff --git a/src/java.base/share/classes/java/lang/Integer.java b/src/java.base/share/classes/java/lang/Integer.java index 92d48c3d78b..4fee928497b 100644 --- a/src/java.base/share/classes/java/lang/Integer.java +++ b/src/java.base/share/classes/java/lang/Integer.java @@ -27,6 +27,7 @@ import jdk.internal.misc.CDS; import jdk.internal.misc.VM; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -1014,6 +1015,7 @@ private IntegerCache() {} * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; diff --git a/src/java.base/share/classes/java/lang/Long.java b/src/java.base/share/classes/java/lang/Long.java index 2b5211dad7a..f4bb60fa973 100644 --- a/src/java.base/share/classes/java/lang/Long.java +++ b/src/java.base/share/classes/java/lang/Long.java @@ -34,6 +34,7 @@ import java.util.Optional; import jdk.internal.misc.CDS; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -1016,6 +1017,7 @@ private LongCache() {} * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Long valueOf(long l) { final int offset = 128; if (l >= -128 && l <= 127) { // will cache diff --git a/src/java.base/share/classes/java/lang/Number.java b/src/java.base/share/classes/java/lang/Number.java index d0ebec608da..ae6537fad6e 100644 --- a/src/java.base/share/classes/java/lang/Number.java +++ b/src/java.base/share/classes/java/lang/Number.java @@ -24,7 +24,6 @@ */ package java.lang; - /** * The abstract class {@code Number} is the superclass of platform * classes representing numeric values that are convertible to the diff --git a/src/java.base/share/classes/java/lang/Short.java b/src/java.base/share/classes/java/lang/Short.java index 6f26a08971a..d7f0d0be017 100644 --- a/src/java.base/share/classes/java/lang/Short.java +++ b/src/java.base/share/classes/java/lang/Short.java @@ -26,6 +26,7 @@ package java.lang; import jdk.internal.misc.CDS; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -34,7 +35,6 @@ import java.util.Optional; import static java.lang.constant.ConstantDescs.BSM_EXPLICIT_CAST; -import static java.lang.constant.ConstantDescs.CD_int; import static java.lang.constant.ConstantDescs.CD_short; import static java.lang.constant.ConstantDescs.DEFAULT_NAME; @@ -274,6 +274,7 @@ private ShortCache() {} * @since 1.5 */ @IntrinsicCandidate + @DeserializeConstructor public static Short valueOf(short s) { final int offset = 128; int sAsInt = s; diff --git a/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java b/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java index 696a0b4a8a2..724d9fc7002 100644 --- a/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java +++ b/src/java.base/share/classes/jdk/internal/reflect/ReflectionFactory.java @@ -351,7 +351,7 @@ private boolean superHasAccessibleConstructor(Class cl) { */ public final Constructor newConstructorForSerialization(Class cl) { if (cl.isValue()) { - throw new UnsupportedOperationException("newConstructorForSerialization does not support value classes"); + throw new UnsupportedOperationException("newConstructorForSerialization does not support value classes: " + cl); } Class initCl = cl; diff --git a/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java b/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java new file mode 100644 index 00000000000..f1acff12831 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/value/DeserializeConstructor.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.value; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; + +/** + * Indicates the constructor or static factory to + * construct a value object during deserialization. + * The annotation is used by java.io.ObjectStreamClass to select the constructor + * or factory method to create objects from a stream. + * + * @since 24 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value={CONSTRUCTOR, METHOD}) +public @interface DeserializeConstructor { +} diff --git a/test/jdk/java/io/Serializable/valueObjects/SerializeAllValueClasses.java b/test/jdk/java/io/Serializable/valueObjects/SerializeAllValueClasses.java new file mode 100644 index 00000000000..7a7b3d826e1 --- /dev/null +++ b/test/jdk/java/io/Serializable/valueObjects/SerializeAllValueClasses.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @modules java.base/jdk.internal java.base/jdk.internal.misc + * @run junit/othervm --enable-preview SerializeAllValueClasses + * @run junit/othervm SerializeAllValueClasses + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.ZonedDateTime; +import java.time.chrono.HijrahDate; +import java.time.chrono.JapaneseDate; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalUnit; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.misc.PreviewFeatures; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Scans all classes in the JDK for those recognized as value classes + * or with the annotation jdk.internal.misc.ValueBasedClass. + * + * Scanning is done over the jrt: filesystem. Classes are matched using the + * following criteria: + * + * - serializable + * - is a public or protected class + * - has public or protected constructor + * + * This returns a list of class', which is convenient for the caller. + */ + +public class SerializeAllValueClasses { + // Cache of instances of known classes suitable as arguments to constructors + // or factory methods. + private static final Map, Object> argumentForType = initInstances(); + + private static Map, Object> initInstances() { + Map, Object> map = new HashMap<>(); + map.put(Integer.class, 12); map.put(int.class, 12); + map.put(Short.class, (short)3); map.put(short.class, (short)3); + map.put(Byte.class, (byte)4); map.put(byte.class, (byte)4); + map.put(Long.class, 5L); map.put(long.class, 5L); + map.put(Character.class, 'C'); map.put(char.class, 'C'); + map.put(Float.class, 1.0f); map.put(float.class, 1.0f); + map.put(Double.class, 2.0d); map.put(double.class, 2.0d); + map.put(Duration.class, Duration.ofHours(1)); + map.put(TemporalUnit.class, ChronoUnit.SECONDS); + map.put(LocalTime.class, LocalTime.of(12, 1)); + map.put(LocalDate.class, LocalDate.of(2024, 1, 1)); + map.put(LocalDateTime.class, LocalDateTime.of(2024, 2, 1, 12, 2)); + map.put(TemporalAccessor.class, ZonedDateTime.now()); + map.put(ZonedDateTime.class, ZonedDateTime.now()); + map.put(Clock.class, Clock.systemUTC()); + map.put(Month.class, Month.JANUARY); + map.put(Instant.class, Instant.now()); + map.put(JapaneseDate.class, JapaneseDate.now()); + map.put(HijrahDate.class, HijrahDate.now()); + return map; + } + + + // Stream the value classes to the test + private static Stream classProvider() throws IOException, URISyntaxException { + return findAll().stream().map(c -> Arguments.of(c)); + } + + @Test + void info() { + var info = (PreviewFeatures.isEnabled()) ? " Checking preview classes declared as `value class`" : + " Checking identity classes with annotation `jdk.internal.ValueBased.class`"; + System.err.println(info); + } + + @ParameterizedTest + @MethodSource("classProvider") + void testValueClass(Class clazz) { + boolean atLeastOne = false; + + Object expected = argumentForType.get(clazz); + if (expected != null) { + serializeDeserialize(expected); + atLeastOne = true; + } + var cons = clazz.getConstructors(); + for (Constructor c : cons) { + Object[] args = makeArgs(c.getParameterTypes(), clazz); + if (args != null) { + try { + expected = c.newInstance(args); + serializeDeserialize(expected); + atLeastOne = true; + break; // one is enough + } catch (InvocationTargetException | InstantiationException | + IllegalAccessException e) { + // Ignore + System.err.printf(""" + Ignoring constructor: %s + Generated arguments are invalid: %s + %s + """, + c, Arrays.toString(args), e.getCause()); + } + } + } + + // Scan for suitable factory methods + for (Method m : clazz.getMethods()) { + if (Modifier.isStatic(m.getModifiers()) && + m.getReturnType().equals(clazz)) { + // static method returning itself + Object[] args = makeArgs(m.getParameterTypes(), clazz); + if (args != null) { + try { + expected = m.invoke(null, args); + serializeDeserialize(expected); + atLeastOne = true; + break; // one is enough + } catch (IllegalAccessException | InvocationTargetException e) { + // Ignore + System.err.printf(""" + Ignoring factory: %s + Generated arguments are invalid: %s + %s + """, + m, Arrays.toString(args), e.getCause()); + } + } + } + } + assertTrue(atLeastOne, "No constructor or factory found for " + clazz); + } + + /** + * {@return an array of instances matching the parameter types, or null} + * + * @param paramTypes an array of parameter types + * @param forClazz the owner class for which the parameters are being generated + */ + private Object[] makeArgs(Class[] paramTypes, Class forClazz) { + Object[] args = Arrays.stream(paramTypes) + .map(t -> makeArg(t, forClazz)) + .toArray(); + for (Object arg : args) { + if (arg == null) + return null; + } + return args; + } + + /** + * {@return an instance of the class, or null if not available} + * String values are customized by the requesting owner. + * For example, "true" is returned as a value when requested for "Boolean". + * @param paramType the parameter type + * @param forClass the owner class + */ + private static Object makeArg(Class paramType, Class forClass) { + return (paramType == String.class || paramType == CharSequence.class) + ? makeStringArg(forClass) + : argumentForType.get(paramType); + } + + /** + * {@return a string representation of an instance of class, or null} + * Mostly special cased for core value classes. + * @param forClass a Class + */ + private static String makeStringArg(Class forClass) { + if (forClass == Integer.class || forClass == int.class || + forClass == Byte.class || forClass == byte.class || + forClass == Short.class || forClass == short.class || + forClass == Long.class || forClass == long.class) { + return "0"; + } else if (forClass == Boolean.class || forClass == boolean.class) { + return "true"; + } else if (forClass == Float.class || forClass == float.class || + forClass == Double.class || forClass == double.class) { + return "1.0"; + } else if (forClass == Duration.class) { + return "PT4H"; + } else if (forClass == LocalDate.class) { + return LocalDate.of(2024, 1, 1).toString(); + } else if (forClass == LocalDateTime.class) { + return LocalDateTime.of(2024, 1, 1, 12, 1).toString(); + } else if (forClass == LocalTime.class) { + return LocalTime.of(12, 1).toString(); + } else if (forClass == Instant.class) { + return Instant.ofEpochSecond(5_000_000, 1000).toString(); + } else { + return null; + } + } + + static final ClassLoader LOADER = SerializeAllValueClasses.class.getClassLoader(); + + private static Optional> findClass(String name) { + try { + Class clazz = Class.forName(name, false, LOADER); + return Optional.of(clazz); + } catch (ClassNotFoundException | ExceptionInInitializerError | + NoClassDefFoundError | IllegalAccessError ex) { + return Optional.empty(); + } + } + + private static boolean isClass(Class clazz) { + return !(clazz.isEnum() || clazz.isInterface()); + } + + private static boolean isNonAbstract(Class clazz) { + return (clazz.getModifiers() & Modifier.ABSTRACT) == 0; + } + + private static boolean isPublicOrProtected(Class clazz) { + return (clazz.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0; + } + + @SuppressWarnings("preview") + private static boolean isValueClass(Class clazz) { + if (PreviewFeatures.isEnabled()) + return clazz.isValue(); + var a = clazz.getAnnotation(jdk.internal.ValueBased.class); + return a != null; + } + + /** + * Scans classes in the JDK and returns matching classes. + * + * @return list of matching class + * @throws IOException if an unexpected exception occurs + * @throws URISyntaxException if an unexpected exception occurs + */ + public static List> findAll() throws IOException, URISyntaxException { + FileSystem fs = FileSystems.getFileSystem(new URI("jrt:/")); + Path dir = fs.getPath("/modules"); + try (final Stream paths = Files.walk(dir)) { + // each path is in the form: /modules////.../name.class + return paths.filter((path) -> path.getNameCount() > 2) + .map((path) -> path.subpath(2, path.getNameCount())) + .map(Path::toString) + .filter((name) -> name.endsWith(".class")) + .map((name) -> name.replaceFirst("\\.class$", "")) + .filter((name) -> !name.equals("module-info")) + .map((name) -> name.replaceAll("/", ".")) + .flatMap((java.lang.String name) -> findClass(name).stream()) + .filter(Serializable.class::isAssignableFrom) + .filter(SerializeAllValueClasses::isClass) + .filter(SerializeAllValueClasses::isNonAbstract) + .filter((klass) -> !klass.isSealed()) + .filter(SerializeAllValueClasses::isValueClass) + .filter(SerializeAllValueClasses::isPublicOrProtected) + .collect(Collectors.toList()); + } + } + + private void serializeDeserialize(Object expected) { + try { + Object actual = deserialize(serialize(expected)); + assertEquals(expected, actual, "round trip compare fail"); + } catch (IOException | ClassNotFoundException e) { + fail("serialize/Deserialize", e); + } + } + + /** + * Serialize an object into byte array. + */ + private static byte[] serialize(Object obj) throws IOException { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + try (ObjectOutputStream out = new ObjectOutputStream(bs)) { + out.writeObject(obj); + } + return bs.toByteArray(); + } + + /** + * Deserialize an object from byte array using the requested classloader. + */ + private static Object deserialize(byte[] ba) throws IOException, ClassNotFoundException { + try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ba))) { + return in.readObject(); + } + } + +} diff --git a/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java b/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java new file mode 100644 index 00000000000..de80da9ae4d --- /dev/null +++ b/test/jdk/java/io/Serializable/valueObjects/SerializedObjectCombo.java @@ -0,0 +1,1373 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import combo.ComboInstance; +import combo.ComboParameter; +import combo.ComboTask; +import combo.ComboTestHelper; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidClassException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OptionalDataException; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; + +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static jdk.test.lib.Asserts.assertFalse; + +import jdk.test.lib.hexdump.HexPrinter; +import jdk.test.lib.hexdump.ObjectStreamPrinter; + +/* + * @test + * @summary Deserialization Combo tests + * @library /test/langtools/tools/javac/lib /test/lib . + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.code + * jdk.compiler/com.sun.tools.javac.comp + * jdk.compiler/com.sun.tools.javac.file + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.tree + * jdk.compiler/com.sun.tools.javac.util + * @build combo.ComboTestHelper SerializedObjectCombo + * @run main/othervm --enable-preview SerializedObjectCombo --everything --no-pre-filter + */ + + +public final class SerializedObjectCombo extends ComboInstance { + private static final Map LOADER_FOR_PATH = new ConcurrentHashMap<>(); + private static final ParamSet KIND_SET = new ParamSet("KIND", + SerializationKind.values()); + private static final ParamSet FIELD_SET = new ParamSet("FIELD", + 2, ArgumentValue.BASIC_VALUES); + private static final ParamSet CLASSACCESS_SET = new ParamSet("CLASSACCESS", + new ClassAccessKind[]{ClassAccessKind.PUBLIC}); + private static final ParamSet SPECIAL_WRITE_METHODS_SET = new ParamSet("SPECIAL_WRITE_METHODS", + WriteObjectFragments.values()); + private static final ParamSet SPECIAL_READ_METHODS_SET = new ParamSet("SPECIAL_READ_METHODS", + ReadObjectFragments.values()); + private static final ParamSet EXTERNALIZABLE_METHODS_SET = new ParamSet("EXTERNALIZABLE_METHODS", + ExternalizableMethodFragments.values()); + private static final ParamSet OBJECT_CONSTRUCTOR_SET = new ParamSet("OBJECT_CONSTRUCTOR", + ObjectConstructorFragment.ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT, ObjectConstructorFragment.NONE); + private static final ParamSet VALUE_SET = new ParamSet("VALUE", + ValueKind.values()); + private static final ParamSet TESTNAME_EXTENDS_SET = new ParamSet("TESTNAME_EXTENDS", + TestNameExtendsFragments.NONE, TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT); + private static final ParamSet TOP_ABSTRACT_SET = new ParamSet("TOP_FRAGMENTS", + TopFragments.values()); + /** + * The base template to generate all test classes. + * Each substitutable fragment is defined by an Enum of the alternatives. + * Giving each a name and an array of ComboParameters with the expansion value. + */ + private static final String TEST_SOURCE_TEMPLATE = """ + import java.io.*; + import java.util.*; + import jdk.internal.value.DeserializeConstructor; + import jdk.internal.MigratedValueClass; + + #{TOP_FRAGMENTS} + + @MigratedValueClass + #{CLASSACCESS} #{VALUE} class #{TESTNAME} #{TESTNAME_EXTENDS} #{KIND.IMPLEMENTS} { + #{FIELD[0]} f1; + #{FIELD[1]} f2; + #{FIELD_ADDITIONS} + #{CLASSACCESS} #{TESTNAME}() { + f1 = #{FIELD[0].RANDOM}; + f2 = #{FIELD[1].RANDOM}; + #{FIELD_CONSTRUCTOR_ADDITIONS} + } + #{OBJECT_CONSTRUCTOR} + @Override public boolean equals(Object obj) { + if (obj instanceof #{TESTNAME} other) { + if (#{FIELD[0]}.class.isPrimitive()) { + if (f1 != other.f1) return false; + } else { + if (!Objects.equals(f1, other.f1)) return false; + } + if (#{FIELD[1]}.class.isPrimitive()) { + if (f2 != other.f2) return false; + } else { + if (!Objects.equals(f2, other.f2)) return false; + } + return true; + } + return false; + } + @Override public String toString() { + return "f1: " + String.valueOf(f1) + + ", f2: " + String.valueOf(f2) + #{FIELD_TOSTRING_ADDITIONS}; + } + #{KIND.SPECIAL_METHODS} + private static final long serialVersionUID = 1L; + } + """; + + // The unique number to qualify interface names, unique across multiple runs + private static int uniqueId = 0; + // Compilation errors prevent execution; set/cleared by checkCompile + private ComboTask.Result compilationResult = null; + // The current set of parameters for the file being compiled and tested + private final Set currParams = new HashSet<>(); + + private static List focusKeys = null; + + private enum CommandOption { + SHOW_SOURCE("--show-source", "show source files"), + VERBOSE("--verbose", "show extra information"), + SHOW_SERIAL_STREAM("--show-serial", "show and format the serialized stream"), + EVERYTHING("--everything", "run all tests"), + TRACE("--trace", "set TRACE system property of ObjectInputStream (temp)"), + MAX_COMBOS("--max-combo", "maximum number of values for each parameter", CommandOption::parseInt), + NO_PRE_FILTER("--no-pre-filter", "disable pre-filter checks"), + SELFTEST("--self-test", "run some self tests and exit"), + ; + private final String option; + private final String usage; + private final BiFunction parseArg; + private Optional value; + CommandOption(String option, String usage, BiFunction parseArg) { + this.option = option; + this.usage = usage; + this.parseArg = parseArg; + this.value = Optional.empty(); + } + CommandOption(String option, String usage) { + this(option, usage, null); + } + + /** + * Evaluate and parse an array of command line args + * @param args array of strings + * @return true if parsing succeeded + */ + static boolean parseOptions(String[] args) { + boolean unknownArg = false; + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + Optional knownOpt = Arrays.stream(CommandOption.values()) + .filter(o -> o.option.equals(arg)) + .findFirst(); + if (knownOpt.isEmpty()) { // Not a recognized option + if (arg.startsWith("-")) { + System.err.println("Unrecognized option: " + arg); + unknownArg = true; + } else { + // Take the remaining non-option args as selectors of keys to be run + String[] keys = Arrays.copyOfRange(args, i, args.length); + focusKeys = List.of(keys); + } + } else { + CommandOption option = knownOpt.get(); + if (option.parseArg == null) { + option.setValue(true); + } else { + i++; + if (i >= args.length || args[i].startsWith("--")) { + System.err.println("Missing argument for " + option.option); + continue; + } + option.parseArg.apply(option, args[i]); + } + } + } + return !unknownArg; + } + static void showUsage() { + System.out.println(""" + Usage: + """); + Arrays.stream(CommandOption.values()).forEach(o -> System.out.printf(" %-15s: %s\n", o.option, o.usage)); + } + boolean present() { + return value != null && value.isPresent(); + } + void setValue(Object o) { + value = Optional.ofNullable(o); + } + private static boolean parseInt(CommandOption option, String arg) { + try { + int count = Integer.parseInt(arg); + option.setValue(count); + } catch (NumberFormatException nfe) { + System.out.println("--max-combo argument not a number: " + arg); + } + return true; + } + // Get the int value from the option, defaulting if not valid or present + private int getInt(int otherMax) { + Object obj = value == null ? otherMax : value.orElseGet(() -> otherMax); + return (obj instanceof Integer i) ? i : otherMax; + } + } + + private static URLClassLoader getLoaderFor(Path path) { + return LOADER_FOR_PATH.computeIfAbsent(path, + p -> { + try { + // new URLClassLoader for path + Files.createDirectories(p); + URL[] urls = {p.toUri().toURL()}; + return new URLClassLoader(p.toString(), urls, null); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + }); + } + + // Map an array of strings to an array of ComboParameter.Constants. + @SuppressWarnings("unchecked") + private static ComboParameter.Constant[] paramsForStrings(String... strings) { + return Arrays.stream(strings) + .map(ComboParameter.Constant::new).toArray(ComboParameter.Constant[]::new); + } + + /** + * Main to generate combinations and run the tests. + * + * @param args may contain "--verbose" to show source of every file + * @throws Exception In case of failure + */ + public static void main(String... args) throws Exception { + if (!CommandOption.parseOptions(args)) { + CommandOption.showUsage(); + System.exit(1); + } + + Arrays.stream(CommandOption.values()) + .filter(o -> o.present()) + .forEach( o1 -> System.out.printf(" %15s: %s\n", o1.option, o1.value + )); + + if (CommandOption.SELFTEST.present()) { + selftest(); + return; + } + + // Sets of all possible ComboParameters (substitutions) + Set allParams = Set.of( + VALUE_SET, + KIND_SET, + TOP_ABSTRACT_SET, + OBJECT_CONSTRUCTOR_SET, + TESTNAME_EXTENDS_SET, + CLASSACCESS_SET, + SPECIAL_READ_METHODS_SET, + SPECIAL_WRITE_METHODS_SET, + EXTERNALIZABLE_METHODS_SET, + FIELD_SET + ); + + // Test variations of all code shapes + var helper = new ComboTestHelper(); + int maxCombos = CommandOption.MAX_COMBOS.getInt(2); + + Set subSet = CommandOption.EVERYTHING.present() ? allParams + : computeSubset(allParams, focusKeys, maxCombos); + withDimensions(helper, subSet); + if (CommandOption.VERBOSE.present()) { + System.out.println("Keys; maximum combinations: " + maxCombos); + subSet.stream() + .sorted((p, q) -> String.CASE_INSENSITIVE_ORDER.compare(p.key(), q.key())) + .forEach(p -> System.out.println(" " + p.key + ": " + Arrays.toString(p.params))); + } + helper.withFilter(SerializedObjectCombo::filter) + .withFailMode(ComboTestHelper.FailMode.FAIL_FAST) + .run(SerializedObjectCombo::new); + } + + private static void withDimensions(ComboTestHelper helper, Set subSet) { + subSet.forEach(p -> { + if (p.count() == 1) + helper.withDimension(p.key(), SerializedObjectCombo::saveParameter, p.params()); + else + helper.withArrayDimension(p.key(), SerializedObjectCombo::saveParameter, p.count(), p.params()); + }); + } + + // Return a subset of ParamSets with the non-focused ParamSet's truncated to a max number of values + private static Set computeSubset(Set allParams, List focusKeys, int maxKeys) { + if (focusKeys == null || focusKeys.isEmpty()) + return allParams; + Set r = allParams.stream().map(p -> + (focusKeys.contains(p.key())) ? p + : new ParamSet(p.key, p.count(), Arrays.copyOfRange(p.params(), 0, Math.min(p.params().length, maxKeys)))) + .collect(Collectors.toUnmodifiableSet()); + return r; + } + + /** + * Print the source files to System out + * + * @param task the compilation task + */ + static void showSources(ComboTask task) { + task.getSources() + .forEach(fo -> { + System.out.println("Source: " + fo.getName()); + System.out.println(getSource(fo)); + }); + } + + /** + * Return the contents of the source file + * + * @param fo a file object + * @return the contents of the source file + */ + static String getSource(JavaFileObject fo) { + try (Reader reader = fo.openReader(true)) { + char[] buf = new char[100000]; + var len = reader.read(buf); + return new String(buf, 0, len); + } catch (IOException ioe) { + return "IOException: " + fo.getName() + ", ex: " + ioe.getMessage(); + } + } + + /** + * Dump the serial stream. + * + * @param bytes the bytes of the stream + */ + private static void showSerialStream(byte[] bytes) { + HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes); + } + + /** + * Serialize an object into byte array. + */ + private static byte[] serialize(Object obj) throws IOException { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + try (ObjectOutputStream out = new ObjectOutputStream(bs)) { + out.writeObject(obj); + } + return bs.toByteArray(); + } + + /** + * Deserialize an object from byte array using the requested classloader. + */ + private static Object deserialize(byte[] ba, ClassLoader loader) throws IOException, ClassNotFoundException { + try (ObjectInputStream in = new LoaderObjectInputStream(new ByteArrayInputStream(ba), loader)) { + return in.readObject(); + } + } + + + @Override + public int id() { + return ++uniqueId; + } + + private void fail(String msg, Throwable thrown) { + super.fail(msg); + thrown.printStackTrace(System.out); + } + + /** + * Save a parameter. + * + * @param param a ComboParameter + */ + private void saveParameter(ComboParameter param) { + saveParameter(param, 0); + } + + /** + * Save an indexed parameter. + * + * @param param a ComboParameter + * @param index unused + */ + private void saveParameter(ComboParameter param, int index) { + currParams.add(param); + } + + /** + * Filter out needless tests (mostly with more variations of arguments than needed). + * Usually, these are compile time failures, or code shapes that cannot succeed. + * + * @return true to run the test, false if not + */ + boolean filter() { + if (!CommandOption.NO_PRE_FILTER.present()) { + for (CodeShape shape : CodeShape.values()) { + if (shape.test(currParams)) { + if (CommandOption.VERBOSE.present()) { + System.out.println("IGNORING: " + shape); + } + return false; + } + } + } + if (CommandOption.VERBOSE.present()) { + System.out.println("TESTING: "); + showParams(); + } + return true; + } + + /** + * Generate the source files from the parameters and test a single combination. + * Two versions are compiled into different directories and separate class loaders. + * They differ only with the addition of a field to the generated class. + * Then each class is serialized and deserialized by the other class, + * testing simple evolution in the process. + * + * @throws IOException catch all IOException + */ + @Override + public void doWork() throws IOException { + String cp = System.getProperty("test.classes"); + String className = "Class_" + this.id(); + + // new URLClassLoader for path + final Path firstPath = Path.of(cp, "1st"); + URLClassLoader firstLoader = getLoaderFor(firstPath); + final Path secondPath = Path.of(cp, "2nd"); + URLClassLoader secondLoader = getLoaderFor(secondPath); + + // Create a map of additional constants that are resolved without the combo overhead. + final Map> params = new HashMap<>(); + params.put("TESTNAME", new ComboParameter.Constant<>(className)); + params.put("SPECIAL_METHODS_SERIALIZABLE", new ComboParameter.Constant<>("#{SPECIAL_READ_METHODS} #{SPECIAL_WRITE_METHODS}")); + params.put("SPECIAL_METHODS_EXTERNALIZABLE", new ComboParameter.Constant<>("#{EXTERNALIZABLE_METHODS}")); + params.put("FIELD_ADDITIONS", new ComboParameter.Constant<>("")); + params.put("FIELD_CONSTRUCTOR_ADDITIONS", new ComboParameter.Constant<>("")); + params.put("FIELD_TOSTRING_ADDITIONS", new ComboParameter.Constant<>("")); + + final ComboTask firstTask = generateAndCompile(firstPath, className, params); + + if (firstTask == null) { + return; // Skip execution, errors already reported + } + + if (CommandOption.EVERYTHING.present()) { + params.put("FIELD_ADDITIONS", new ComboParameter.Constant<>("int fExtra;")); + params.put("FIELD_CONSTRUCTOR_ADDITIONS", new ComboParameter.Constant<>("this.fExtra = 99;")); + params.put("FIELD_TOSTRING_ADDITIONS", new ComboParameter.Constant<>("+ \", fExtra: String.valueOf(fExtra)\"")); + final ComboTask secondTask = generateAndCompile(secondPath, className, params); + if (secondTask == null) { + return; // Skip execution, errors already reported + } + + doTestWork(className, firstTask, firstLoader, secondLoader); + doTestWork(className, secondTask, secondLoader, firstLoader); + } else { + doTestWork(className, firstTask, firstLoader, firstLoader); + } + } + + /** + * Test that two versions of the class can be serialized using one version and deserialized + * by the other version. + * The two classes have the same name and have been compiled into different classloaders. + * The original and result objects are compared using .equals if there is only 1 classloader. + * If the classloaders are different the `toString()` output for each object is compared loosely. + * (One must be the prefix of the other) + * + * @param className the class name + * @param task the task context (for source and parameters to report failures) + * @param firstLoader the first classloader + * @param secondLoader the second classloader + */ + private void doTestWork(String className, ComboTask task, ClassLoader firstLoader, ClassLoader secondLoader) { + byte[] bytes = null; + try { + Class tc = Class.forName(className, true, firstLoader); + Object testObj = tc.getDeclaredConstructor().newInstance(); + bytes = serialize(testObj); + if (CommandOption.VERBOSE.present()) { + System.out.println("Testing: " + task.getSources()); + if (CommandOption.SHOW_SOURCE.present()) { + showParams(); + showSources(task); + } + if (CommandOption.SHOW_SERIAL_STREAM.present()) { + showSerialStream(bytes); + } + } + + if (CodeShape.BAD_SO_CONSTRUCTOR.test(currParams)) { + // should have thrown ICE due to mismatch between value class and missing constructor + System.out.println(CodeShape.BAD_SO_CONSTRUCTOR.explain(currParams)); + fail(CodeShape.BAD_SO_CONSTRUCTOR.explain(currParams)); + } + + Object actual = deserialize(bytes, secondLoader); + if (testObj.getClass().getClassLoader().equals(actual.getClass().getClassLoader())) { + assertEquals(testObj, actual, "Round-trip comparison fail using .equals"); + } else { + // The instances are from different classloaders and can't be compared directly + final String s1 = testObj.toString(); + final String s2 = actual.toString(); + assertTrue(s1.startsWith(s2) || s2.startsWith(s1), + "Round-trip comparison fail using toString(): s1: " + s1 + ", s2: " + s2); + } + } catch (InvalidClassException ice) { + for (CodeShape shape : CodeShape.values()){ + if (ice.equals(shape.exception)) { + if (shape.test(currParams)) { + if (CommandOption.VERBOSE.present()) { + System.out.println("OK: " + shape.explain(currParams)); + } else { + // unexpected ICE + ice.printStackTrace(System.out); + showParams(); + showSources(task); + if (bytes != null) + showSerialStream(bytes); + fail(ice.getMessage()); + } + } + } + } + } catch (EOFException | OptionalDataException eof) { + // Ignore if conditions of the source invite EOF + if (0 == CodeShape.shapesThrowing(EOFException.class).peek(s -> { + // Ignore: Serialized Object to reads custom data but none written + if (CommandOption.VERBOSE.present()) { + System.out.println("OK: " + s.explain(currParams)); + } + }).count()) { + eof.printStackTrace(System.out); + showParams(); + showSources(task); + showSerialStream(bytes); + fail(eof.getMessage(), eof); + } + } catch (ClassFormatError cfe) { + System.out.println(cfe.toString()); + } catch (NotSerializableException nse) { + if (CodeShape.BAD_EXT_VALUE.test(currParams)) { + // Expected Value class that is Externalizable w/o writeReplace + } else { + // unexpected NSE + nse.printStackTrace(System.out); + showParams(); + showSources(task); + fail(nse.getMessage(), nse); + } + } catch (Throwable ex) { + ex.printStackTrace(System.out); + showParams(); + showSources(task); + fail(ex.getMessage()); + } + } + + // Side effect of error is compilationResult.hasErrors() > 0 + private ComboTask generateAndCompile(Path path, String className, Map> params) { + ComboTask task = newCompilationTask() + .withSourceFromTemplate(className, + TEST_SOURCE_TEMPLATE, + r -> params.computeIfAbsent(r, s -> new ComboParameter.Constant<>("UNKNOWN_" + s))) + .withOption("-d") + .withOption(path.toString()) + .withOption("--enable-preview") + .withOption("--add-modules") + .withOption("java.base") + .withOption("--add-exports") + .withOption("java.base/jdk.internal=ALL-UNNAMED") + .withOption("--add-exports") + .withOption("java.base/jdk.internal.value=ALL-UNNAMED") + .withOption("--source") + .withOption(Integer.toString(Runtime.version().feature())); + ; + task.generate(this::checkCompile); + if (compilationResult.hasErrors()) { + boolean match = false; + for (CodeShape shape : CodeShape.values()){ + if (CompileException.class.equals(shape.exception)) { + if (shape.test(currParams)) { + // shape matches known error + if (!uniqueParams.contains(shape)) { + System.out.println("// Unique: " + shape); + uniqueParams.add(shape); + } + match = true; + } + } + } + if (match) + return null; + // Unexpected compilation error + showDiags(compilationResult); + showSources(task); + showParams(); + fail("Compilation failure"); + } + return task; + } + + private static Set uniqueParams = new HashSet<>(); + + private String paramToString(ComboParameter param) { + String name = param.getClass().getName(); + return name.substring(name.indexOf('$') + 1) + "::" + + param + ": " + truncate(param.expand(null), 60); + } + + private void showParams() { + currParams.stream() + .sorted((p, q) -> String.CASE_INSENSITIVE_ORDER.compare(paramToString(p), paramToString(q))) + .forEach(p -> System.out.println(" " + paramToString(p))); + } + + private void showParams(ComboParameter... params) { + for (ComboParameter param : params) { + System.out.println(">>> " + paramToString(param) + ", present: " + + currParams.contains(param)); + } + } + + private static String truncate(String s, int maxLen) { + int nl = s.indexOf("\n"); + if (nl >= 0) + maxLen = nl; + if (maxLen < s.length()) { + return s.substring(0, maxLen).concat("..."); + } else { + return s; + } + } + + /** + * Report any compilation errors. + * + * @param res the result + */ + void checkCompile(ComboTask.Result res) { + compilationResult = res; + } + + void showDiags(ComboTask.Result res) { + res.diagnosticsForKind(Diagnostic.Kind.ERROR).forEach(SerializedObjectCombo::showDiag); + res.diagnosticsForKind(Diagnostic.Kind.WARNING).forEach(SerializedObjectCombo::showDiag); + } + + static void showDiag(Diagnostic diag) { + System.out.println(diag.getKind() + ": " + diag.getMessage(Locale.ROOT)); + System.out.println("File: " + diag.getSource() + + " line: " + diag.getLineNumber() + ", col: " + diag.getColumnNumber()); + } + + private static class CodeShapePredicateOp implements Predicate { + private final Predicate first; + private final Predicate other; + private final String op; + + CodeShapePredicateOp(Predicate first, Predicate other, String op) { + if ("OR" != op && "AND" != op && "NOT" != op) + throw new IllegalArgumentException("unknown op: " + op); + this.first = first; + this.other = other; + this.op = op; + } + + @Override + public boolean test(T comboParameters) { + return switch (op) { + case "NOT" -> !first.test(comboParameters); + case "OR" -> first.test(comboParameters) || other.test(comboParameters); + case "AND" -> first.test(comboParameters) && other.test(comboParameters); + default -> throw new IllegalArgumentException("unknown op: " + op); + }; + } + @Override + public Predicate and(Predicate other) { + return new CodeShapePredicateOp(this, other,"AND"); + } + + + @Override + public Predicate negate() { + return new CodeShapePredicateOp(this, null,"NOT"); + } + + @Override + public Predicate or(Predicate other) { + return new CodeShapePredicateOp(this, other,"OR"); + } + public String toString() { + return switch (op) { + case "NOT" -> op + " " + first; + case "OR" -> "(" + first + " " + op + " " + other + ")"; + case "AND" -> "(" + first + " " + op + " " + other + ")"; + default -> throw new IllegalArgumentException("unknown op: " + op); + }; + } + } + + interface CodeShapePredicate extends Predicate> { + @Override + default boolean test(Set comboParameters) { + return comboParameters.contains(this); + } + + @Override + default Predicate> and(Predicate> other) { + return new CodeShapePredicateOp(this, other,"AND"); + } + + + @Override + default Predicate> negate() { + return new CodeShapePredicateOp(this, null,"NOT"); + } + + @Override + default Predicate> or(Predicate> other) { + return new CodeShapePredicateOp(this, other,"OR"); + } + } + + /** + * A set of code shapes that are interesting, usually indicating an error + * compile time, or runtime based on the shape of the code and the dependencies between + * the code fragments. + * The descriptive text may be easier to understand than the boolean expression of the fragments. + * They can also be to filter out test cases that would not succeed. + * Or can be used after a successful deserialization to check + * if an exception should have been thrown. + */ + private enum CodeShape implements Predicate> { + BAD_SO_CONSTRUCTOR("Value class does not have a constructor annotated with DeserializeConstructor", + InvalidClassException.class, + ValueKind.VALUE, + ObjectConstructorFragment.ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT.negate() + ), + BAD_EXT_VALUE("Externalizable can not be a value class", + CompileException.class, + SerializationKind.EXTERNALIZABLE, + ValueKind.VALUE), + BAD_EXT_METHODS("Externalizable methods but not Externalizable", + CompileException.class, + ExternalizableMethodFragments.EXTERNALIZABLE_METHODS, + SerializationKind.EXTERNALIZABLE.negate()), + BAD_EXT_NO_METHODS("Externalizable but no implementation of readExternal or writeExternal", + CompileException.class, + SerializationKind.EXTERNALIZABLE, + ExternalizableMethodFragments.EXTERNALIZABLE_METHODS.negate()), + BAD_VALUE_NON_ABSTRACT_SUPER("Can't inherit from non-abstract super or abstract super with fields", + CompileException.class, + ValueKind.VALUE, + TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT, + TopFragments.ABSTRACT_NO_FIELDS.negate()), + BAD_MISSING_SUPER("Extends TOP_ without TOP_ superclass", + CompileException.class, + TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT, + TopFragments.NONE), + BAD_READ_CUSTOM_METHODS("Custom read fragment but no custom write fragment", + EOFException.class, + ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT + .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT), + WriteObjectFragments.WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT + .or(WriteObjectFragments.WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT).negate() + ), + BAD_RW_CUSTOM_METHODS("Custom write fragment but no custom read fragment", + null, + WriteObjectFragments.WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT + .or(WriteObjectFragments.WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT), + ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT + .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT).negate()), + BAD_VALUE_READOBJECT_METHODS("readObjectXXX(OIS) methods incompatible with Value class", + CompileException.class, + ReadObjectFragments.READ_OBJECT_FIELDS_FRAGMENT + .or(ReadObjectFragments.READ_OBJECT_DEFAULT_FRAGMENT) + .or(ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT) + .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT), + ValueKind.VALUE), + ; + + private final String description; + private final Class exception; + private final List>> predicates; + CodeShape(String desc, Class exception, Predicate>... predicates) { + this.description = desc; + this.exception = exception; + this.predicates = List.of(predicates); + } + + // Return a stream of CodeShapes throwing the exception + static Stream shapesThrowing(Class exception) { + return Arrays.stream(values()).filter(s -> exception.equals(s.exception)); + + } + + /** + * {@return true if all of the predicates are true in the set of ComboParameters} + * @param comboParameters a set of ComboParameters + */ + @Override + public boolean test(Set comboParameters) { + for (Predicate> p : predicates) { + if (!p.test(comboParameters)) + return false; + } + return true; + } + + /** + * {@return a string describing the predicate in relation to a set of parameters} + * @param comboParameters a set of active ComboParameters. + */ + public String explain(Set comboParameters) { + StringBuffer sbTrue = new StringBuffer(); + StringBuffer sbFalse = new StringBuffer(); + for (Predicate> p : predicates) { + ((p.test(comboParameters)) ? sbTrue : sbFalse) + .append(p).append(", "); + } + return description + "\n" +"Missing: " + sbFalse + "\nTrue: " + sbTrue; + } + public String toString() { + return super.toString() + "::" + description + ", params: " + predicates; + } + } + + /** + * TopAbstract Fragments + */ + enum TopFragments implements ComboParameter, CodeShapePredicate { + NONE(""), + ABSTRACT_NO_FIELDS(""" + @MigratedValueClass + abstract #{VALUE} class TOP_#{TESTNAME} implements Serializable { + #{CLASSACCESS} TOP_#{TESTNAME}() {} + } + """), + ABSTRACT_ONE_FIELD(""" + @MigratedValueClass + abstract #{VALUE} class TOP_#{TESTNAME} implements Serializable { + private int t1; + #{CLASSACCESS} TOP_#{TESTNAME}() { + t1 = 1; + } + } + """), + NO_FIELDS(""" + @MigratedValueClass + #{VALUE} class TOP_#{TESTNAME} implements Serializable { + #{CLASSACCESS} TOP_#{TESTNAME}() {} + } + """), + ONE_FIELD(""" + @MigratedValueClass + #{VALUE} class TOP_#{TESTNAME} implements Serializable { + private int t1; + #{CLASSACCESS} TOP_#{TESTNAME}() { + t1 = 1; + } + } + """), + ; + + private final String template; + + TopFragments(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * TopAbstract Fragments + */ + enum TestNameExtendsFragments implements ComboParameter, CodeShapePredicate { + NONE(""), + TESTNAME_EXTENDS_FRAGMENT("extends TOP_#{TESTNAME}"), + ; + + private final String template; + + TestNameExtendsFragments(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * SerializedObjectCustom Fragments + */ + enum SerializedObjectCustomFragments implements ComboParameter, CodeShapePredicate { + NONE(""), + ; + + private final String template; + + SerializedObjectCustomFragments(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * ExternalizableMethod Fragments + */ + enum ExternalizableMethodFragments implements ComboParameter, CodeShapePredicate { + NONE(""), + EXTERNALIZABLE_METHODS(""" + public void writeExternal(ObjectOutput oos) throws IOException { + oos.write#{FIELD[0].READFIELD}(f1); + oos.write#{FIELD[1].READFIELD}(f2); + } + + public void readExternal(ObjectInput ois) throws IOException, ClassNotFoundException { + f1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}(); + f2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}(); + } + """), + ; + + private final String template; + + ExternalizableMethodFragments(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * ObjectConstructorFragment Fragments + */ + enum ObjectConstructorFragment implements ComboParameter, CodeShapePredicate { + NONE(""), + ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT(""" + @DeserializeConstructor + #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2) { + this.f1 = f1; + this.f2 = f2; + #{FIELD_CONSTRUCTOR_ADDITIONS} + } + + @DeserializeConstructor + #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2, int fExtra) { + this.f1 = f1; + this.f2 = f2; + #{FIELD_CONSTRUCTOR_ADDITIONS} + } + """), + + ; + + private final String template; + + ObjectConstructorFragment(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * WriteObject templates + */ + enum WriteObjectFragments implements ComboParameter, CodeShapePredicate { + NONE(""), + WRITE_OBJECT_DEFAULT_FRAGMENT(""" + private void writeObject(ObjectOutputStream oos) throws IOException { + oos.defaultWriteObject(); + } + """), + WRITE_OBJECT_FIELDS_FRAGMENT(""" + private void writeObject(ObjectOutputStream oos) throws IOException { + ObjectOutputStream.PutField fields = oos.putFields(); + fields.put("f1", f1); + fields.put("f2", f2); + oos.writeFields(); + } + """), + WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT(""" + private void writeObject(ObjectOutputStream oos) throws IOException { + oos.defaultWriteObject(); + // Write custom data + oos.write#{FIELD[0].READFIELD}(#{FIELD[0].DEFAULT}); + oos.write#{FIELD[1].READFIELD}(#{FIELD[1].DEFAULT}); + } + """), + WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT(""" + private void writeObject(ObjectOutputStream oos) throws IOException { + ObjectOutputStream.PutField fields = oos.putFields(); + fields.put("f1", f1); + fields.put("f2", f2); + oos.writeFields(); + // Write custom data + oos.write#{FIELD[0].READFIELD}(#{FIELD[0].DEFAULT}); + oos.write#{FIELD[1].READFIELD}(#{FIELD[1].DEFAULT}); + } + """), + ; + + private final String template; + + WriteObjectFragments(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * ReadObject templates + */ + enum ReadObjectFragments implements ComboParameter, CodeShapePredicate { + NONE(""), + READ_OBJECT_DEFAULT_FRAGMENT(""" + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + } + """), + READ_OBJECT_FIELDS_FRAGMENT(""" + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField fields = ois.readFields(); + this.f1 = (#{FIELD[0]})fields.get("f1", #{FIELD[0].DEFAULT}); + this.f2 = (#{FIELD[1]})fields.get("f2", #{FIELD[1].DEFAULT}); + } + """), + READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT(""" + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + // Read custom data + #{FIELD[0]} d1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}(); + #{FIELD[1]} d2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}(); + assert Objects.equals(#{FIELD[0].DEFAULT}, d1) : "reading custom data1, actual: " + d1; + assert Objects.equals(#{FIELD[1].DEFAULT}, d2) : "reading custom data2, actual: " + d2; + } + """), + READ_OBJECT_FIELDS_CUSTOM_FRAGMENT(""" + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField fields = ois.readFields(); + this.f1 = (#{FIELD[0]})fields.get("f1", #{FIELD[0].DEFAULT}); + this.f2 = (#{FIELD[1]})fields.get("f2", #{FIELD[1].DEFAULT}); + // Read custom data + #{FIELD[0]} d1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}(); + #{FIELD[1]} d2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}(); + assert Objects.equals(#{FIELD[0].DEFAULT}, d1) : "reading custom data1, actual: " + d1; + assert Objects.equals(#{FIELD[1].DEFAULT}, d2) : "reading custom data2, actual: " + d2; + } + """), + ; + + private final String template; + + ReadObjectFragments(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + /** + * Value and Identity kinds. + */ + enum ValueKind implements ComboParameter, CodeShapePredicate { + VALUE("value"), + IDENTITY(""), + ; + + private final String template; + + ValueKind(String template) { + this.template = template; + } + + @Override + public String expand(String optParameter) { + return template; + } + } + + enum SerializationKind implements ComboParameter, CodeShapePredicate { + SERIALIZABLE("SER", "implements Serializable"), + EXTERNALIZABLE("EXT", "implements Externalizable"), + ; + + private final String key; + private final String declaration; + + SerializationKind(String key, String declaration) { + this.key = key; + this.declaration = declaration; + } + + public String expand(String optParameter) { + return switch (optParameter) { + case null -> key; + case "IMPLEMENTS" -> declaration; + default -> + "#{" + optParameter + "_" + this + "}"; // everything ELSE turn into requested key with suffix + }; + } + } + + /** + * Class Access kinds. + */ + enum ClassAccessKind implements ComboParameter, CodeShapePredicate { + PUBLIC("public"), + PACKAGE(""), + ; + + private final String classAccessTemplate; + + ClassAccessKind(String classAccessTemplate) { + this.classAccessTemplate = classAccessTemplate; + } + + @Override + public String expand(String optParameter) { + return classAccessTemplate; + } + } + + /** + * Type of arguments to insert in method signatures + */ + enum ArgumentValue implements ComboParameter, CodeShapePredicate { + BOOLEAN("boolean", true), + BYTE("byte", (byte) 127), + CHAR("char", 'Z'), + SHORT("short", (short) 0x7fff), + INT("int", 0x7fffffff), + LONG("long", 0x7fffffffffffffffL), + FLOAT("float", 1.0F), + DOUBLE("double", 1.0d), + STRING("String", "xyz"); + + static final ArgumentValue[] BASIC_VALUES = {INT, STRING}; + + private final String argumentsValueTemplate; + private final Object value; + + ArgumentValue(String argumentsValueTemplate, Object value) { + this.argumentsValueTemplate = argumentsValueTemplate; + this.value = value; + } + + @Override + public String expand(String optParameter) { + return switch (optParameter) { + case null -> argumentsValueTemplate; + case "TITLECASE" -> Character.toTitleCase(argumentsValueTemplate.charAt(0)) + + argumentsValueTemplate.substring(1); + case "DEFAULT" -> switch (this) { + case BOOLEAN -> "false"; + case BYTE -> "(byte)-1"; + case CHAR -> "'" + "!" + "'"; + case SHORT -> "(short)-1"; + case INT -> "-1"; + case LONG -> "-1L"; + case FLOAT -> "-1.0f"; + case DOUBLE -> "-1.0d"; + case STRING -> '"' + "n/a" + '"'; + }; + case "READFIELD" -> switch (this) { + case BOOLEAN -> "Boolean"; + case BYTE -> "Byte"; + case CHAR -> "Char"; + case SHORT -> "Short"; + case INT -> "Int"; + case LONG -> "Long"; + case FLOAT -> "Float"; + case DOUBLE -> "Double"; + case STRING -> "Object"; + }; + case "RANDOM" -> switch (this) { // or can be Random + case BOOLEAN -> Boolean.toString(!(boolean) value); + case BYTE -> "(byte)" + value + 1; + case CHAR -> "'" + value + "'"; + case SHORT -> "(short)" + value + 1; + case INT -> "-2"; + case LONG -> "-2L"; + case FLOAT -> (1.0f + (float) value) + "f"; + case DOUBLE -> (1.0d + (float) value) + "d"; + case STRING -> "\"" + value + "!\""; + }; + default -> switch (this) { + case BOOLEAN -> value.toString(); + case BYTE -> "(byte)" + value; + case CHAR -> "'" + value + "'"; + case SHORT -> "(short)" + value; + case INT -> "-1"; + case LONG -> "-1L"; + case FLOAT -> value + "f"; + case DOUBLE -> value + "d"; + case STRING -> '"' + (String) value + '"'; + }; + }; + } + } + + /** + * Set of Parameters to fill in template. + * + * @param key the key + * @param params the ComboParameters (one or more) + */ + record ParamSet(String key, int count, ComboParameter... params) { + /** + * Set of parameter strings for fill in template. + * The strings are mapped to CompboParameter.Constants. + * + * @param key the key + * @param strings varargs strings + */ + ParamSet(String key, String... strings) { + this(key, 1, paramsForStrings(strings)); + } + + /** + * Set of parameter strings for fill in template. + * The strings are mapped to CompboParameter.Constants. + * + * @param key the key + * @param strings varargs strings + */ + ParamSet(String key, int count, String... strings) { + this(key, count, paramsForStrings(strings)); + } + + /** + * Set of parameters for fill in template. + * The strings are mapped to CompboParameter.Constants. + * + * @param key the key + * @param params varargs strings + */ + ParamSet(String key, ComboParameter... params) { + this(key, 1, params); + } + } + + /** + * Marks conditions that should match compile time errors + */ + static class CompileException extends RuntimeException { + CompileException(String msg) { + super(msg); + } + } + + /** + * Custom ObjectInputStream to be resolve classes from a specific class loader. + */ + private static class LoaderObjectInputStream extends ObjectInputStream { + private final ClassLoader loader; + + public LoaderObjectInputStream(InputStream in, ClassLoader loader) throws IOException { + super(in); + this.loader = loader; + } + + /** + * Override resolveClass to be resolve classes from the specified loader. + * + * @param desc an instance of class {@code ObjectStreamClass} + * @return the class + * @throws ClassNotFoundException if the class is not found + */ + @Override + protected Class resolveClass(ObjectStreamClass desc) throws ClassNotFoundException { + String name = desc.getName(); + try { + return Class.forName(name, false, loader); + } catch (ClassNotFoundException ex) { + Class cl = Class.forPrimitiveName(name); + if (cl != null) { + return cl; + } else { + throw ex; + } + } + } + } + + private abstract class MyCompilationTask extends ComboInstance { + + } + private static void selftest() { + Set params = Set.of(ValueKind.VALUE, SerializationKind.EXTERNALIZABLE); + assertTrue(ValueKind.VALUE.test(params), "VALUE"); + assertTrue(SerializationKind.EXTERNALIZABLE.test(params), "SerializationKind.EXTERNALIZABLE"); + assertFalse(CodeShape.BAD_EXT_VALUE.test(params)); + } +} diff --git a/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java b/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java index f9ebdea81e4..182da307310 100644 --- a/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java +++ b/test/jdk/java/io/Serializable/valueObjects/SimpleValueGraphs.java @@ -27,6 +27,7 @@ * @summary Serialize and deserialize value objects * @enablePreview * @modules java.base/jdk.internal + * @modules java.base/jdk.internal.value * @run testng/othervm SimpleValueGraphs */ @@ -40,7 +41,6 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.InvalidClassException; -import java.io.NotSerializableException; import java.util.Arrays; import java.util.function.BiFunction; @@ -48,6 +48,7 @@ import java.nio.charset.StandardCharsets; +import jdk.internal.value.DeserializeConstructor; import jdk.internal.MigratedValueClass; import org.testng.Assert; @@ -60,7 +61,7 @@ @Test public class SimpleValueGraphs implements Serializable { - private static boolean DEBUG = false; + private static boolean DEBUG = true; private static SimpleValue foo1 = new SimpleValue("One", 1); private static SimpleValue foo2 = new SimpleValue("Two", 2); @@ -109,7 +110,7 @@ public Object[][] migrationObjects() { } /** - * Test serializing a object graph, and deserialize with a modification of the serialized form. + * Test serializing an object graph, and deserialize with a modification of the serialized form. * The modifications to the stream change the class name being deserialized. * The cases include serializing an identity class and deserialize the corresponding * value class. @@ -129,7 +130,7 @@ public void treeVTest(Object origObj, String origName, String replName, Object e } // Modify the serialized bytes to change a class name from the serialized name - // to a different class. The replacement name must be the same length as thr original name. + // to a different class. The replacement name must be the same length as the original name. byte[] replBytes = patchBytes(bytes, origName, replName); if (DEBUG) { System.out.println("Modified serialized " + origObj.getClass().getName()); @@ -145,8 +146,9 @@ public void treeVTest(Object origObj, String origName, String replName, Object e "Resulting object not equals: " + actual.getClass().getName()); } catch (Exception ex) { - Assert.assertEquals(ex.getClass(), expectedObject.getClass(), "exception type"); - Assert.assertEquals(ex.getMessage(), ((Exception)expectedObject).getMessage(), "exception message"); + ex.printStackTrace(); + Assert.assertEquals(ex.getClass(), expectedObject.getClass(), ex.toString()); + Assert.assertEquals(ex.getMessage(), ((Exception)expectedObject).getMessage(), ex.toString()); } } @@ -178,7 +180,7 @@ private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoun } /** - * Replace every occurence of the string in the byte array with the replacement. + * Replace every occurrence of the string in the byte array with the replacement. * The strings are US_ASCII only. * @param bytes a byte array * @param orig a string, converted to bytes using US_ASCII, originally exists in the bytes @@ -193,7 +195,7 @@ private byte[] patchBytes(byte[] bytes, String orig, String repl) { } /** - * Replace every occurence of the original bytes in the byte array with the replacement bytes. + * Replace every occurrence of the original bytes in the byte array with the replacement bytes. * @param bytes a byte array * @param orig a byte array containing existing bytes in the byte array * @param repl a byte array to replace the original bytes @@ -287,7 +289,6 @@ public void setRight(TreeI right) { } public boolean equals(Object other) { - // avoid ==, is substutible check causes stack overflow. if (other instanceof TreeV tree) { boolean leftEq = (this.left == null && tree.left == null) || left.equals(tree.left); @@ -309,13 +310,14 @@ public String toString(int depth) { } } - @jdk.internal.MigratedValueClass + @MigratedValueClass static value class TreeV implements Tree, Serializable { private static final long serialVersionUID = 2L; private TreeV left; private TreeV right; + @DeserializeConstructor TreeV(TreeV left, TreeV right) { this.left = left; this.right = right; @@ -336,7 +338,7 @@ public boolean equals(Object other) { return false; } - // Compare references but don't use ==; isSubstutitable may recurse + // Compare references but don't use ==; isSubstitutable may recurse private static boolean compRef(Object o1, Object o2) { if (o1 == null && o2 == null) return true; @@ -360,8 +362,9 @@ public String toString(int depth) { @Test void testExternalizableNotSer() { var obj = new ValueExt(); - var ex = Assert.expectThrows(NotSerializableException.class, () -> serialize(obj)); - Assert.assertEquals(ex.getMessage(), ValueExt.class.getName()); + var ex = Assert.expectThrows(InvalidClassException.class, () -> serialize(obj)); + Assert.assertEquals(ex.getMessage(), + "SimpleValueGraphs$ValueExt; Externalizable not valid for value class"); } @Test @@ -386,6 +389,7 @@ public void readExternal(ObjectInput is) { private static final long serialVersionUID = 3L; } + // Not Desrializable or Deserializable, no writeable fields static value class ValueExt implements Externalizable { public void writeExternal(ObjectOutput is) { diff --git a/test/jdk/java/io/Serializable/valueObjects/ValueSerialization.java b/test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java similarity index 50% rename from test/jdk/java/io/Serializable/valueObjects/ValueSerialization.java rename to test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java index 37909d35832..f6ea5f2d847 100644 --- a/test/jdk/java/io/Serializable/valueObjects/ValueSerialization.java +++ b/test/jdk/java/io/Serializable/valueObjects/ValueSerializationTest.java @@ -23,13 +23,15 @@ /* * @test - * @summary ValueSerialization support of value classes + * @summary Test serialization of value classes * @enablePreview - * @modules java.base/jdk.internal - * @compile ValueSerialization.java - * @run testng/othervm ValueSerialization + * @modules java.base/jdk.internal java.base/jdk.internal.value + * @compile ValueSerializationTest.java + * @run testng/othervm ValueSerializationTest */ +import static java.io.ObjectStreamConstants.*; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; @@ -44,42 +46,48 @@ import java.io.ObjectOutputStream; import java.io.ObjectStreamClass; import java.io.ObjectStreamException; +import java.io.Serial; import java.io.Serializable; + +import jdk.internal.MigratedValueClass; +import jdk.internal.value.DeserializeConstructor; + +import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.io.ObjectStreamConstants.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; -import static org.testng.Assert.expectThrows; -public class ValueSerialization { +public class ValueSerializationTest { static final Class NSE = NotSerializableException.class; + private static final Class ICE = InvalidClassException.class; @DataProvider(name = "doesNotImplementSerializable") public Object[][] doesNotImplementSerializable() { return new Object[][] { - new Object[] { new NonSerializablePoint(10, 100) }, + new Object[] { new NonSerializablePoint(10, 100), NSE}, + new Object[] { new NonSerializablePointNoCons(10, 100), ICE}, // an array of Points - new Object[] { new NonSerializablePoint[] {new NonSerializablePoint(1, 5)} }, - new Object[] { new Object[] {new NonSerializablePoint(3, 7)} }, - new Object[] { new ExternalizablePoint(12, 102) }, + new Object[] { new NonSerializablePoint[] {new NonSerializablePoint(1, 5)}, NSE}, + new Object[] { new Object[] {new NonSerializablePoint(3, 7)}, NSE}, + new Object[] { new ExternalizablePoint(12, 102), ICE}, new Object[] { new ExternalizablePoint[] { new ExternalizablePoint(3, 7), - new ExternalizablePoint(2, 8) } }, + new ExternalizablePoint(2, 8) }, ICE}, new Object[] { new Object[] { new ExternalizablePoint(13, 17), - new ExternalizablePoint(14, 18) } }, + new ExternalizablePoint(14, 18) }, ICE}, }; } - // value class that DOES NOT implement Serializable should throw NSE + // value class that DOES NOT implement Serializable should throw ICE @Test(dataProvider = "doesNotImplementSerializable") - public void doesNotImplementSerializable(Object obj) { - assertThrows(NSE, () -> serialize(obj)); + public void doesNotImplementSerializable(Object obj, Class expectedException) { + assertThrows(expectedException, () -> serialize(obj)); } - /** Non-Serializable point. */ + /* Non-Serializable point. */ public static value class NonSerializablePoint { public int x; public int y; @@ -88,32 +96,31 @@ public NonSerializablePoint(int x, int y) { this.x = x; this.y = y; } - } - - /** A Serializable value class Point */ - @jdk.internal.MigratedValueClass - static value class SerializablePoint implements Serializable { - public int x; - public int y; - SerializablePoint(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { - return "[SerializablePoint x=" + x + " y=" + y + "]"; } + return "[NonSerializablePoint x=" + x + " y=" + y + "]"; + } } - /** A Serializable value class Point */ - @jdk.internal.MigratedValueClass - static value class SerializablePrimitivePoint implements Serializable { + /* Non-Serializable point, because it does not have an @DeserializeConstructor constructor. */ + public static value class NonSerializablePointNoCons implements Serializable { public int x; public int y; - SerializablePrimitivePoint(int x, int y) { this.x = x; this.y = y; } + + // Note: Must NOT have @DeserializeConstructor annotation + public NonSerializablePointNoCons(int x, int y) { + this.x = x; + this.y = y; + } @Override public String toString() { - return "[SerializablePrimitivePoint x=" + x + " y=" + y + "]"; } + return "[NonSerializablePointNoCons x=" + x + " y=" + y + "]"; + } } - /** An Externalizable Point is not Serializable, readExternal cannot modify fields */ + /* An Externalizable Point is not Serializable, readExternal cannot modify fields */ static value class ExternalizablePoint implements Externalizable { public int x; public int y; + public ExternalizablePoint() {this.x = 0; this.y = 0;} ExternalizablePoint(int x, int y) { this.x = x; this.y = y; } @Override public void readExternal(ObjectInput in) { } @Override public void writeExternal(ObjectOutput out) { } @@ -121,101 +128,90 @@ static value class ExternalizablePoint implements Externalizable { return "[ExternalizablePoint x=" + x + " y=" + y + "]"; } } - @DataProvider(name = "doesImplementSerializable") - public Object[][] doesImplementSerializable() { - return new Object[][] { - new Object[] { new SerializablePoint(11, 101) }, - new Object[] { new SerializablePoint[] { - new SerializablePoint(1, 5), - new SerializablePoint(2, 6) } }, - new Object[] { new Object[] { - new SerializablePoint(3, 7), - new SerializablePoint(4, 8) } }, - new Object[] { new SerializablePrimitivePoint(711, 7101) }, - new Object[] { new SerializablePrimitivePoint[] { - new SerializablePrimitivePoint(71, 75), - new SerializablePrimitivePoint(72, 76) } }, - new Object[] { new Object[] { - new SerializablePrimitivePoint(73, 77), - new SerializablePrimitivePoint(74, 78) } }, + @DataProvider(name = "ImplementSerializable") + public Object[][] implementSerializable() { + return new Object[][]{ + new Object[]{new SerializablePoint(11, 101)}, + new Object[]{new SerializablePoint[]{ + new SerializablePoint(1, 5), + new SerializablePoint(2, 6)}}, + new Object[]{new Object[]{ + new SerializablePoint(3, 7), + new SerializablePoint(4, 8)}}, + new Object[]{new SerializableFoo(45)}, + new Object[]{new SerializableFoo[]{new SerializableFoo(46)}}, + new Object[]{new ExternalizableFoo("hello")}, + new Object[]{new ExternalizableFoo[]{new ExternalizableFoo("there")}}, }; } - // value class that DOES implement Serializable all supported - @Test(dataProvider = "doesImplementSerializable") - public void doesImplementSerializable(Object obj) throws IOException { - serialize(obj); + // value class that DOES implement Serializable is supported + @Test(dataProvider = "ImplementSerializable") + public void implementSerializable(Object obj) throws IOException, ClassNotFoundException { + byte[] bytes = serialize(obj); + Object actual = deserialize(bytes); + if (obj.getClass().isArray()) + assertEquals((Object[])obj, (Object[])actual); + else + assertEquals(obj, actual); + } + + /* A Serializable value class Point */ + @MigratedValueClass + static value class SerializablePoint implements Serializable { + public int x; + public int y; + @DeserializeConstructor + private SerializablePoint(int x, int y) { this.x = x; this.y = y; } + + @Override public String toString() { + return "[SerializablePoint x=" + x + " y=" + y + "]"; + } } - /** A Serializable Foo, with a serial proxy */ + /* A Serializable Foo, with a serial proxy */ static value class SerializableFoo implements Serializable { public int x; + @DeserializeConstructor SerializableFoo(int x) { this.x = x; } - Object writeReplace() throws ObjectStreamException { + @Serial Object writeReplace() throws ObjectStreamException { return new SerialFooProxy(x); } - private void readObject(ObjectInputStream s) throws InvalidObjectException { + @Serial private void readObject(ObjectInputStream s) throws InvalidObjectException { throw new InvalidObjectException("Proxy required"); } - private static class SerialFooProxy implements Serializable { - final int x; - SerialFooProxy(int x) { this.x = x; } - Object readResolve() throws ObjectStreamException { - return new SerializableFoo(this.x); + private record SerialFooProxy(int x) implements Serializable { + @Serial Object readResolve() throws ObjectStreamException { + return new SerializableFoo(x); } } } - /** An Externalizable Foo, with a serial proxy */ + /* An Externalizable Foo, with a serial proxy */ static value class ExternalizableFoo implements Externalizable { public String s; ExternalizableFoo(String s) { this.s = s; } - Object writeReplace() throws ObjectStreamException { - return new SerialFooProxy(s); + public boolean equals(Object other) { + if (other instanceof ExternalizableFoo foo) { + return s.equals(foo.s); + } else { + return false; + } } - private void readObject(ObjectInputStream s) throws InvalidObjectException { - throw new InvalidObjectException("Proxy required"); + @Serial Object writeReplace() throws ObjectStreamException { + return new SerialFooProxy(s); } - private static class SerialFooProxy implements Serializable { - final String s; - SerialFooProxy(String s) { this.s = s; } - Object readResolve() throws ObjectStreamException { - return new ExternalizableFoo(this.s); + private record SerialFooProxy(String s) implements Serializable { + @Serial Object readResolve() throws ObjectStreamException { + return new ExternalizableFoo(s); } } @Override public void readExternal(ObjectInput in) { } @Override public void writeExternal(ObjectOutput out) { } } - // value classes that DO implement Serializable, but have a serial proxy - @Test - public void serializableFooWithProxy() throws Exception { - SerializableFoo foo = new SerializableFoo(45); - SerializableFoo foo1 = serializeDeserialize(foo); - assertEquals(foo.x, foo1.x); - } - @Test - public void serializableFooArrayWithProxy() throws Exception { - SerializableFoo[] fooArray = new SerializableFoo[]{new SerializableFoo(46)}; - SerializableFoo[] fooArray1 = serializeDeserialize(fooArray); - assertEquals(fooArray.length, fooArray1.length); - assertEquals(fooArray[0].x, fooArray1[0].x); - } - @Test - public void externalizableFooWithProxy() throws Exception { - ExternalizableFoo foo = new ExternalizableFoo("hello"); - ExternalizableFoo foo1 = serializeDeserialize(foo); - assertEquals(foo.s, foo1.s); - } - @Test - public void externalizableFooArrayWithProxy() throws Exception { - ExternalizableFoo[] fooArray = new ExternalizableFoo[] { new ExternalizableFoo("there") }; - ExternalizableFoo[] fooArray1 = serializeDeserialize(fooArray); - assertEquals(fooArray.length, fooArray1.length); - assertEquals(fooArray[0].s, fooArray1[0].s); - } - + // Generate a byte stream containing a reference to the named class with the SVID and flags. private static byte[] byteStreamFor(String className, long uid, byte flags) throws Exception { @@ -235,38 +231,31 @@ private static byte[] byteStreamFor(String className, long uid, byte flags) return baos.toByteArray(); } - private static byte[] serializableByteStreamFor(String className, long uid) - throws Exception - { - return byteStreamFor(className, uid, SC_SERIALIZABLE); - } - - private static byte[] externalizableByteStreamFor(String className, long uid) - throws Exception - { - return byteStreamFor(className, uid, SC_EXTERNALIZABLE); - } - @DataProvider(name = "classes") public Object[][] classes() { return new Object[][] { - new Object[] { NonSerializablePoint.class }, - new Object[] { SerializablePoint.class }, - new Object[] { SerializablePrimitivePoint.class } + new Object[] { ExternalizableFoo.class, SC_EXTERNALIZABLE, ICE }, + new Object[] { ExternalizableFoo.class, SC_SERIALIZABLE, ICE }, + new Object[] { SerializablePoint.class, SC_EXTERNALIZABLE, ICE }, + new Object[] { SerializablePoint.class, SC_SERIALIZABLE, null }, }; } - static final Class ICE = InvalidClassException.class; - - // value class read directly from a byte stream, both serializable and externalizable are supported + // value class read directly from a byte stream + // a byte stream is generated containing a reference to the class with the flags and SVID. + // Reading the class from the stream verifies the exceptions thrown if there is a mismatch + // between the stream and the local class. @Test(dataProvider = "classes") - public void deserialize(Class cls) throws Exception { + public void deserialize(Class cls, byte flags, Class expected) throws Exception { var clsDesc = ObjectStreamClass.lookup(cls); long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID(); - - byte[] serialBytes = serializableByteStreamFor(cls.getName(), uid); - - byte[] extBytes = externalizableByteStreamFor(cls.getName(), uid); + byte[] serialBytes = byteStreamFor(cls.getName(), uid, flags); + try { + deserialize(serialBytes); + Assert.assertNull(expected, "Expected exception"); + } catch (IOException ioe) { + Assert.assertEquals(ioe.getClass(), expected); + } } static byte[] serialize(T obj) throws IOException { @@ -285,11 +274,4 @@ static T deserialize(byte[] streamBytes) ObjectInputStream ois = new ObjectInputStream(bais); return (T) ois.readObject(); } - - @SuppressWarnings("unchecked") - static T serializeDeserialize(T obj) - throws IOException, ClassNotFoundException - { - return (T) deserialize(serialize(obj)); - } }