Skip to content

Commit

Permalink
Merge pull request #118 from dmlloyd/records
Browse files Browse the repository at this point in the history
[JBMAR-241] [JBMAR-254] Add record support to serializing cloner
  • Loading branch information
ropalka committed Sep 20, 2023
2 parents 5cfc5ff + 1562926 commit 304c457
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 20 deletions.
39 changes: 28 additions & 11 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,35 @@
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<trimStackTrace>false</trimStackTrace>
</configuration>
<executions>
<execution>
<id>default-test</id>
<configuration>
<classesDirectory>${project.build.directory}/classes/META-INF/versions/9</classesDirectory>
<additionalClasspathElements>
<additionalClasspathElement>${project.build.directory}/classes</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>


<profiles>
<profile>
<id>java16-test-classpath</id>
<activation>
<jdk>[16,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>default-test</id>
<configuration>
<classesDirectory>${project.build.outputDirectory}/META-INF/versions/16</classesDirectory>
<additionalClasspathElements>
<additionalClasspathElement>${project.build.outputDirectory}</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,66 @@ private Object clone(final Object orig, final boolean replace) throws IOExceptio
final SerializableClass cloneInfo = sameClass ? info : registry.lookup(clonedClass);
// Now check the serializable types
final Object clone;
if (orig instanceof Externalizable) {
if (cloneInfo.isRecord()) {
// follow record ctor protocol
SerializableField[] fields = cloneInfo.getFields();
Object[] args = new Object[fields.length];
if (info.isRecord()) {
for (SerializableField field : info.getFields()) {
SerializableField cloneField = cloneInfo.getSerializableFieldByName(field.getName());
if (cloneField != null) {
args[cloneField.getRecordComponentIndex()] = field.getRecordComponentValue(orig);
}
}
} else {
// not a record, just make a best effort
for (SerializableField field : info.getFields()) {
SerializableField cloneField = cloneInfo.getSerializableFieldByName(field.getName());
if (cloneField != null) {
switch (field.getKind()) {
case BOOLEAN: {
args[cloneField.getRecordComponentIndex()] = Boolean.valueOf(field.getBoolean(orig));
break;
}
case BYTE: {
args[cloneField.getRecordComponentIndex()] = Byte.valueOf(field.getByte(orig));
break;
}
case CHAR: {
args[cloneField.getRecordComponentIndex()] = Character.valueOf(field.getChar(orig));
break;
}
case DOUBLE: {
args[cloneField.getRecordComponentIndex()] = Double.valueOf(field.getDouble(orig));
break;
}
case FLOAT: {
args[cloneField.getRecordComponentIndex()] = Float.valueOf(field.getFloat(orig));
break;
}
case INT: {
args[cloneField.getRecordComponentIndex()] = Integer.valueOf(field.getInt(orig));
break;
}
case LONG: {
args[cloneField.getRecordComponentIndex()] = Long.valueOf(field.getLong(orig));
break;
}
case SHORT: {
args[cloneField.getRecordComponentIndex()] = Short.valueOf(field.getShort(orig));
break;
}
case OBJECT: {
args[cloneField.getRecordComponentIndex()] = field.getObject(orig);
break;
}
}
}
}
}
clone = cloneInfo.invokeRecordCanonicalConstructor(args);
clones.put(orig, clone);
} else if (orig instanceof Externalizable) {
final Externalizable externalizable = (Externalizable) orig;
clone = cloneInfo.callNoArgConstructor();
clones.put(orig, clone);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,24 @@ public int compare(final SerializableField o1, final SerializableField o2) {
* An empty array of fields.
*/
public static final SerializableField[] NOFIELDS = new SerializableField[0];
private static final IdentityHashMap<Class<?>, Constructor<?>> EMPTY_IHM = new IdentityHashMap<>(0);

SerializableClass(Class<?> subject) {
this.subject = subject;
final IdentityHashMap<Class<?>, Constructor<?>> constructorMap = new IdentityHashMap<Class<?>, Constructor<?>>();
for (Class<?> t = subject.getSuperclass(); t != null; t = t.getSuperclass()) {
final SerializableClass lookedUp = REGISTRY.lookup(t);
final Constructor<?> constructor = lookedUp.serMethods.getNoArgConstructor();
if (constructor != null) {
constructorMap.put(t, JDKSpecific.newConstructorForSerialization(subject, constructor));
isRecord = JDKSpecific.isRecord(subject);
if (isRecord) {
nonInitConstructors = EMPTY_IHM;
} else {
final IdentityHashMap<Class<?>, Constructor<?>> constructorMap = new IdentityHashMap<Class<?>, Constructor<?>>();
for (Class<?> t = subject.getSuperclass(); t != null; t = t.getSuperclass()) {
final SerializableClass lookedUp = REGISTRY.lookup(t);
final Constructor<?> constructor = lookedUp.serMethods.getNoArgConstructor();
if (constructor != null) {
constructorMap.put(t, JDKSpecific.newConstructorForSerialization(subject, constructor));
}
}
nonInitConstructors = constructorMap;
}
nonInitConstructors = constructorMap;
isRecord = JDKSpecific.isRecord(subject);
// private methods
serMethods = new JDKSpecific.SerMethods(subject);
final ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(subject);
Expand Down Expand Up @@ -195,6 +200,17 @@ public SerializableField getSerializableField(String name, Class<?> fieldType, b
return new SerializableField(fieldType, name, unshared, null, null);
}

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

/**
* Determine whether this class has a {@code writeObject()} method.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.RetentionPolicy;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.TimeUnit;

import java.util.Date;
import org.jboss.marshalling.Pair;
import org.testng.SkipException;
import org.testng.annotations.Test;

import static org.testng.Assert.*;
Expand Down Expand Up @@ -237,6 +241,40 @@ public void testObjectWithSelectiveSerialization() throws Throwable {
assertEquals(clone.test, "foo");
}

@Test
public void testRecordCloning() throws Throwable {
if (Runtime.version().compareTo(Runtime.Version.parse("16")) < 0) {
throw new SkipException("Skipped when JDK < 16");
}
// define a class for it
MethodHandles.Lookup lookup = MethodHandles.lookup();
/* package org.jboss.marshalling.cloner;
public record MyRecord(String stringVal,
char charVal,
long longVal,
int intVal,
short shortVal,
byte byteVal,
float floatVal,
double doubleVal,
boolean booleanVal
) {
} */
@SuppressWarnings("SpellCheckingInspection")
byte[] recordBytes = Base64.getDecoder().decode("yv66vgAAADwAaAoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvUmVjb3JkAQAGPGluaXQ+AQADKClWCQAIAAkHAAoMAAsADAEAJW9yZy9qYm9zcy9tYXJzaGFsbGluZy9jbG9uZXIvTXlSZWNvcmQBAAlzdHJpbmdWYWwBABJMamF2YS9sYW5nL1N0cmluZzsJAAgADgwADwAQAQAHY2hhclZhbAEAAUMJAAgAEgwAEwAUAQAHbG9uZ1ZhbAEAAUoJAAgAFgwAFwAYAQAGaW50VmFsAQABSQkACAAaDAAbABwBAAhzaG9ydFZhbAEAAVMJAAgAHgwAHwAgAQAHYnl0ZVZhbAEAAUIJAAgAIgwAIwAkAQAIZmxvYXRWYWwBAAFGCQAIACYMACcAKAEACWRvdWJsZVZhbAEAAUQJAAgAKgwAKwAsAQAKYm9vbGVhblZhbAEAAVoSAAAALgwALwAwAQAIdG9TdHJpbmcBADsoTG9yZy9qYm9zcy9tYXJzaGFsbGluZy9jbG9uZXIvTXlSZWNvcmQ7KUxqYXZhL2xhbmcvU3RyaW5nOxIAAAAyDAAzADQBAAhoYXNoQ29kZQEAKihMb3JnL2pib3NzL21hcnNoYWxsaW5nL2Nsb25lci9NeVJlY29yZDspSRIAAAA2DAA3ADgBAAZlcXVhbHMBADwoTG9yZy9qYm9zcy9tYXJzaGFsbGluZy9jbG9uZXIvTXlSZWNvcmQ7TGphdmEvbGFuZy9PYmplY3Q7KVoBAB0oTGphdmEvbGFuZy9TdHJpbmc7Q0pJU0JGRFopVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAnTG9yZy9qYm9zcy9tYXJzaGFsbGluZy9jbG9uZXIvTXlSZWNvcmQ7AQAQTWV0aG9kUGFyYW1ldGVycwEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQADKClJAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaAQABbwEAEkxqYXZhL2xhbmcvT2JqZWN0OwEAAygpQwEAAygpSgEAAygpUwEAAygpQgEAAygpRgEAAygpRAEAAygpWgEAClNvdXJjZUZpbGUBAA1NeVJlY29yZC5qYXZhAQAGUmVjb3JkAQAQQm9vdHN0cmFwTWV0aG9kcw8GAFEK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");
Class<?> myRecordClass = lookup.defineClass(recordBytes);
assert myRecordClass.getName().equals("org.jboss.marshalling.cloner.MyRecord");
Constructor<?> ctor = myRecordClass.getConstructor(String.class, char.class, long.class, int.class, short.class, byte.class, float.class, double.class, boolean.class);
Object instance = ctor.newInstance("String", 'X', 1234L, 6543, (short) 123, (byte) 12, 1.4f, 2.6, true);
final ObjectClonerFactory clonerFactory = ObjectCloners.getSerializingObjectClonerFactory();
final ClonerConfiguration configuration = new ClonerConfiguration();
final ObjectCloner cloner = clonerFactory.createCloner(configuration);
Object clone = cloner.clone(instance);
assertNotNull(clone);
assertEquals(clone, instance);
}

public static class CloneTesterWithSerializeOnlyVariable implements Serializable {
private String test;
private String tmp;
Expand Down

0 comments on commit 304c457

Please sign in to comment.