/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.shrinkwrap.impl.base.serialization;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamConstants;
import java.io.ObjectStreamField;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.logging.Logger;
import junit.framework.TestCase;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.serialization.SerializableView;
import org.jboss.shrinkwrap.api.serialization.ZipSerializableView;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* Ensures that serialization of Archives is possible via the {@link SerializableView}s.
*
* SHRINKWRAP-178
*
* @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
* @version $Revision: $
*/
public class SerializationTestCase {
// -------------------------------------------------------------------------------------||
// Class Members -----------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Logger
*/
private static final Logger log = Logger.getLogger(SerializationTestCase.class.getName());
/**
* Name of the payload archive used in testing serialization
*/
private static final String NAME_PAYLOAD_ARCHIVE = "serializedArchive.jar";
// -------------------------------------------------------------------------------------||
// Instance Members --------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* A populated archive to be used in testing serialization
*/
private JavaArchive payload;
// -------------------------------------------------------------------------------------||
// Lifecycle ---------------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Creates a payload archive to be used in serialization tests
*/
@Before
public void createPayload() {
payload = ShrinkWrap.create(JavaArchive.class, NAME_PAYLOAD_ARCHIVE).addClasses(SerializationTestCase.class,
JavaArchive.class);
}
// -------------------------------------------------------------------------------------||
// Tests -------------------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Ensures we may serialize an {@link Archive} as {@link ZipSerializableView} and preserve contents as expected
*/
@Test
public void zipSerializableView() throws Exception {
this.testSerializableView(ZipSerializableView.class);
}
/**
* Ensures we may serialize an {@link Archive} as {@link SerializableView} and preserve contents as expected
*/
@Test
public void serializableView() throws Exception {
this.testSerializableView(SerializableView.class);
}
/**
* Tests that the payload archive may be serialized as the specified {@link SerializableView} type and contents of
* the roundtrip are as expected.
*
* @param <S>
* @param serializableView
* @throws Exception
*/
private <S extends SerializableView> void testSerializableView(final Class<S> serializableView) throws Exception {
// Define the initial archive
log.info("Before: " + payload.toString(true));
// Serialize
final JavaArchive roundtrip = serializeAndDeserialize(payload.as(serializableView)).as(JavaArchive.class);
log.info("After: " + roundtrip.toString(true));
// Ensure contents are as expected
this.testCurrentFields(payload, roundtrip);
}
/**
* Ensures that the current serialization protocol is compatible with the version initially released. We accomplish
* this by mocking {@link ZipSerializableOriginalImpl} and redefining its class name via
* {@link SerializationTestCase#serializeAndDeserialize(ZipSerializableView, Class)}, which uses the
* {@link SpoofingObjectOutputStream}.
*
* @throws Exception
*/
@Test
public void zipWireProtocolCurrentToOriginal() throws Exception {
final SerializableView currentWireFormat = this.payload.as(SerializableView.class);
final SerializableView roundtrip = this.testWireProtocol(currentWireFormat, ZipSerializableOriginalImpl.class);
this.testOriginalFields(payload, roundtrip.as(JavaArchive.class));
}
/**
* Ensures that the original serialization protocol is compatible with the current version. We accomplish this by
* mocking {@link ZipSerializableOriginalImpl} and redefining its class name via
* {@link SerializationTestCase#serializeAndDeserialize(ZipSerializableView, Class)}, which uses the
* {@link SpoofingObjectOutputStream}.
*
* @throws Exception
*/
@Test
public void zipWireProtocolOriginalToCurrent() throws Exception {
final SerializableView originalWireFormat = new ZipSerializableOriginalImpl(payload);
final SerializableView roundtrip = this.testWireProtocol(originalWireFormat, ZipSerializableViewImpl.class);
this.testOriginalFields(payload, roundtrip.as(JavaArchive.class));
}
// -------------------------------------------------------------------------------------||
// Internal Helper Methods -------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Ensures that the specified client object may be serialized to the specified type
*
* @param clientObject
* The object to be serialized
* @param targetType
* The type we should be represented as
* @return The roundtrip view
* @throws IOException
*/
private SerializableView testWireProtocol(final SerializableView clientObject,
final Class<? extends SerializableView> targetType) throws IOException {
// Roundtrip the object, now representing as the target type
final SerializableView roundtrip = serializeAndDeserialize(clientObject, targetType);
// The type of the object put through roundtrip serialization must be of the type specified
TestCase.assertEquals(targetType, roundtrip.getClass());
// Return
return roundtrip;
}
private void testOriginalFields(final Archive<?> payload, final Archive<?> roundtrip) {
final Map<ArchivePath, Node> originalContents = payload.getContent();
final Map<ArchivePath, Node> roundtripContents = roundtrip.getContent();
Assert.assertEquals("Contents after serialization were not as expected", originalContents, roundtripContents);
Assert.assertEquals("Name of original archive was not as expected", NAME_PAYLOAD_ARCHIVE, payload.getName());
Assert.assertEquals("Name not as expected after serialization", payload.getName(), roundtrip.getName());
}
private void testCurrentFields(final Archive<?> payload, final Archive<?> roundtrip) {
this.testOriginalFields(payload, roundtrip);
Assert.assertEquals("ID not as expected after serialization", payload.getId(), roundtrip.getId());
}
/**
* Roundtrip serializes/deserializes the specified {@link Archive}
*
* @param archive
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
private static SerializableView serializeAndDeserialize(final SerializableView archive) throws IOException,
ClassNotFoundException {
assert archive != null : "Archive must be specified";
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
final ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(archive);
out.flush();
out.close();
final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
final SerializableView roundtrip = (SerializableView) in.readObject();
in.close();
return roundtrip;
}
/**
* Roundtrip serializes/deserializes the specified {@link SerializableView} and reconsitutes/redefines as the
* specified target type
*
* @param archive
* The original {@link SerializableView} instance
* @param The
* new type we should cast to after deserialization
* @see http://crazybob.org/2006/01/unit-testing-serialization-evolution.html
* @see http://crazybob.org/2006/01/unit-testing-serialization-evolution_13.html
* @see http://www.theserverside.com/news/thread.tss?thread_id=38398
* @author Bob Lee
*/
private static <S extends SerializableView> S serializeAndDeserialize(final SerializableView archive,
final Class<S> targetType) throws IOException {
final ByteArrayOutputStream bout = new ByteArrayOutputStream();
final ObjectOutputStream oout = new SpoofingObjectOutputStream(bout, archive.getClass(), targetType);
oout.writeObject(archive);
oout.flush();
oout.close();
final ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
final ObjectInputStream oin = new ObjectInputStream(bin);
try {
final Object obj = oin.readObject();
oin.close();
log.info("Original type " + archive.getClass().getName() + " now represented as "
+ obj.getClass().getName());
return targetType.cast(obj);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// -------------------------------------------------------------------------------------||
// Internal Helper Classes -------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* SpoofingObjectOutputStream
*
* ObjectOutputStream which will replace a class name with one explicitly given
*
* @see http://crazybob.org/2006/01/unit-testing-serialization-evolution_13.html
* @author Bob Lee
* @version $Revision: $
*/
static class SpoofingObjectOutputStream extends ObjectOutputStream {
String oldName;
String newName;
public SpoofingObjectOutputStream(OutputStream out, Class<?> oldClass, Class<?> newClass) throws IOException {
super(out);
this.oldName = oldClass.getName();
this.newName = newClass.getName();
}
@Override
protected void writeClassDescriptor(ObjectStreamClass descriptor) throws IOException {
Class<?> clazz = descriptor.forClass();
boolean externalizable = Externalizable.class.isAssignableFrom(clazz);
boolean serializable = Serializable.class.isAssignableFrom(clazz);
boolean hasWriteObjectData = hasWriteObjectMethod(clazz);
boolean isEnum = Enum.class.isAssignableFrom(clazz);
writeUTF(replace(descriptor.getName()));
writeLong(descriptor.getSerialVersionUID());
byte flags = 0;
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
} else if (serializable) {
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
flags |= ObjectStreamConstants.SC_ENUM;
}
writeByte(flags);
ObjectStreamField[] fields = descriptor.getFields();
writeShort(fields.length);
for (ObjectStreamField field : fields) {
writeByte(field.getTypeCode());
writeUTF(field.getName());
if (!field.isPrimitive()) {
writeObject(replace(field.getTypeString()));
}
}
}
String replace(String className) {
if (className.equals(newName)) {
throw new RuntimeException("Found instance of " + className + "." + " Expected instance of " + oldName
+ ".");
}
return className == oldName ? newName : className;
}
boolean hasWriteObjectMethod(Class<?> clazz) {
try {
Method method = clazz.getDeclaredMethod("writeObject", ObjectOutputStream.class);
int modifiers = method.getModifiers();
return method.getReturnType() == Void.TYPE && !Modifier.isStatic(modifiers)
&& Modifier.isPrivate(modifiers);
} catch (NoSuchMethodException e) {
return false;
}
}
}
}