Skip to content

Commit

Permalink
hibernate#87 - Make sure models are serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
sebersole committed Oct 11, 2024
1 parent cd70d95 commit a492193
Show file tree
Hide file tree
Showing 24 changed files with 751 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import java.util.List;

import org.hibernate.models.internal.ClassDetailsSupport;
import org.hibernate.models.internal.SerialCassDetails;
import org.hibernate.models.internal.jdk.SerialJdkCassDetails;
import org.hibernate.models.internal.util.CollectionHelper;
import org.hibernate.models.spi.ClassDetails;
import org.hibernate.models.spi.FieldDetails;
Expand Down Expand Up @@ -276,4 +278,10 @@ private static List<TypeVariableDetails> determineTypeParameters(ClassInfo class
}
return result;
}

@Override
public SerialCassDetails toSerialForm(SourceModelBuildingContext context) {
final Class<Object> classForName = context.getClassLoading().classForName( getClassName() );
return new SerialJdkCassDetails( classForName.getName(), classForName );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import static org.hibernate.models.internal.ModelsClassLogging.MODELS_CLASS_LOGGER;

/**
* SourceModelBuildingContext implementation based on Jandex
*
* @author Steve Ebersole
*/
public class JandexModelBuildingContextImpl extends AbstractModelBuildingContext implements JandexModelBuildingContext {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.models;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Serializable;

/**
* Assists with the serialization process and performs additional
* functionality based on serialization.
* <p>
* <ul>
* <li>Deep clone using serialization
* <li>Serialize managing finally and IOException
* <li>Deserialize managing finally and IOException
* </ul>
* <p>
* This class throws exceptions for invalid {@code null} inputs.
*
* @author Nissim Karpenstein
* @author Janek Bogucki
* @author Daniel Rall
* @author Stephen Colebourne
* @author Jeff Varszegi
* @author Gary Gregory
*
* @since 1.0
*/
public final class SerializationHelper {
private SerializationHelper() {
}

// Clone
//-----------------------------------------------------------------------

/**
* Deep clone an object using serialization.
* <p>
* This is many times slower than writing clone methods by hand
* on all objects in your object graph. However, for complex object
* graphs, or for those that don't support deep cloning this can
* be a simple alternative implementation. Of course all the objects
* must be {@code Serializable}.
*
* @param object the {@code Serializable} object to clone
*
* @return the cloned object
*/
public static <T extends Serializable> T clone(T object) {
if ( object == null ) {
return null;
}
return deserialize( serialize( object ), object.getClass().getClassLoader() );
}

// Serialize
//-----------------------------------------------------------------------

/**
* <p>Serializes an object to the given stream.
* <p>
* The stream will be closed once the object is written.
* This avoids the need for a finally clause, and maybe also
* for exception handling, in the application code.
* <p>
* The stream passed in is not buffered internally within this
* method. This is the responsibility of the caller, if desired.
*
* @param obj the object to serialize to bytes, may be null
* @param outputStream the stream to write to, must not be null
*
* @throws IllegalArgumentException if {@code outputStream} is null
*/
public static void serialize(Serializable obj, OutputStream outputStream) {
if ( outputStream == null ) {
throw new IllegalArgumentException( "The OutputStream must not be null" );
}

ObjectOutputStream out = null;
try {
// stream closed in the finally
out = new ObjectOutputStream( outputStream );
out.writeObject( obj );

}
catch (IOException ex) {
throw new RuntimeException( "could not serialize", ex );
}
finally {
try {
if ( out != null ) {
out.close();
}
}
catch (IOException ignored) {
}
}
}

/**
* Serializes an object to a byte array for storage or
* externalization.
*
* @param obj the object to serialize to bytes
*
* @return a byte[] with the converted Serializable
*/
public static byte[] serialize(Serializable obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream( 512 );
serialize( obj, byteArrayOutputStream );
return byteArrayOutputStream.toByteArray();
}

// Deserialize
//-----------------------------------------------------------------------

/**
* Deserializes an object from the given stream using the
* Thread Context ClassLoader (TCCL).
* <p>
* Delegates to {@link #doDeserialize}
*
* @param inputStream the serialized object input stream, must not be null
*
* @return the deserialized object
*
* @throws IllegalArgumentException if {@code inputStream} is null
*/
public static <T extends Serializable> T deserialize(InputStream inputStream) {
return doDeserialize( inputStream, defaultClassLoader(), hibernateClassLoader(), null );
}

/**
* Returns the Thread Context ClassLoader (TCCL).
*
* @return The current TCCL
*/
public static ClassLoader defaultClassLoader() {
return Thread.currentThread().getContextClassLoader();
}

public static ClassLoader hibernateClassLoader() {
return SerializationHelper.class.getClassLoader();
}

/**
* Deserializes an object from the given stream using the
* Thread Context ClassLoader (TCCL). If there is no TCCL set,
* the classloader of the calling class is used.
* <p>
* The stream will be closed once the object is read. This
* avoids the need for a finally clause, and maybe also for
* exception handling, in the application code.
* <p>
* The stream passed in is not buffered internally within this
* method. This is the responsibility of the caller, if desired.
*
* @param inputStream the serialized object input stream, must not be null
* @param loader The classloader to use
*
* @return the deserialized object
*
* @throws IllegalArgumentException if <code>inputStream</code> is <code>null</code>
*/
public static Object deserialize(InputStream inputStream, ClassLoader loader) {
return doDeserialize( inputStream, loader, defaultClassLoader(), hibernateClassLoader() );
}

@SuppressWarnings("unchecked")
public static <T> T doDeserialize(
InputStream inputStream,
ClassLoader loader,
ClassLoader fallbackLoader1,
ClassLoader fallbackLoader2) {
if ( inputStream == null ) {
throw new IllegalArgumentException( "The InputStream must not be null" );
}

try {
CustomObjectInputStream in = new CustomObjectInputStream(
inputStream,
loader,
fallbackLoader1,
fallbackLoader2
);
try {
return (T) in.readObject();
}
catch (ClassNotFoundException | IOException e) {
throw new RuntimeException( "could not deserialize", e );
}
finally {
try {
in.close();
}
catch (IOException ignore) {
// ignore
}
}
}
catch (IOException e) {
throw new RuntimeException( "could not deserialize", e );
}
}

/**
* Deserializes an object from an array of bytes using the
* Thread Context ClassLoader (TCCL). If there is no TCCL set,
* the classloader of the calling class is used.
* <p>
* Delegates to {@link #deserialize(byte[], ClassLoader)}
*
* @param objectData the serialized object, must not be null
*
* @return the deserialized object
*
* @throws IllegalArgumentException if <code>objectData</code> is <code>null</code>
*/
public static <T extends Serializable> T deserialize(byte[] objectData) {
return doDeserialize( wrap( objectData ), defaultClassLoader(), hibernateClassLoader(), null );
}

private static InputStream wrap(byte[] objectData) {
if ( objectData == null ) {
throw new IllegalArgumentException( "The byte[] must not be null" );
}
return new ByteArrayInputStream( objectData );
}

/**
* Deserializes an object from an array of bytes.
* <p>
* Delegates to {@link #deserialize(InputStream, ClassLoader)} using a
* {@link ByteArrayInputStream} to wrap the array.
*
* @param objectData the serialized object, must not be null
* @param loader The classloader to use
*
* @return the deserialized object
*
* @throws IllegalArgumentException if <code>objectData</code> is <code>null</code>
*/
public static <T extends Serializable> T deserialize(byte[] objectData, ClassLoader loader) {
return doDeserialize( wrap( objectData ), loader, defaultClassLoader(), hibernateClassLoader() );
}


/**
* By default, to resolve the classes being deserialized JDK serialization uses the
* classes loader which loaded the class which initiated the deserialization call. Here
* that would be Hibernate classes. However, there are cases where that is not the correct
* class loader to use; mainly here we are worried about deserializing user classes in
* environments (app servers, etc) where Hibernate is on a parent classes loader. To
* facilitate for that we allow passing in the class loader we should use.
*/
private static final class CustomObjectInputStream extends ObjectInputStream {
private final ClassLoader loader1;
private final ClassLoader loader2;
private final ClassLoader loader3;

private CustomObjectInputStream(
InputStream in,
ClassLoader loader1,
ClassLoader loader2,
ClassLoader loader3) throws IOException {
super( in );
this.loader1 = loader1;
this.loader2 = loader2;
this.loader3 = loader3;
}

@Override
protected Class resolveClass(ObjectStreamClass v) throws IOException, ClassNotFoundException {
final String className = v.getName();

try {
return Class.forName( className, false, loader1 );
}
catch (ClassNotFoundException ignored) {
}

if ( different( loader1, loader2 ) ) {
try {
return Class.forName( className, false, loader2 );
}
catch (ClassNotFoundException ignored) {
}
}

if ( different( loader1, loader3 ) && different( loader2, loader3 ) ) {
try {
return Class.forName( className, false, loader3 );
}
catch (ClassNotFoundException ignored) {
}
}

// By default delegate to normal JDK deserialization which will use the class loader
// of the class which is calling this deserialization.
return super.resolveClass( v );
}

private boolean different(ClassLoader one, ClassLoader other) {
if ( one == null ) {
return other != null;
}
return !one.equals( other );
}
}
}
6 changes: 5 additions & 1 deletion hibernate-models/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ plugins {
id "published-java-module"
}

description = "A de-typed abstraction over reflection and annotations"
description = "A de-typed abstraction over reflection and annotations"

dependencies {
testImplementation project( ":hibernate-models-testing" )
}
Loading

0 comments on commit a492193

Please sign in to comment.