/*
* Copyright 2004-2012 the original author or authors.
*
* 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.springframework.webflow.execution.repository.snapshot;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.InputStream;
import java.io.NotSerializableException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.webflow.execution.FlowExecution;
/**
* A snapshot implementation that is based on standard Java serialization, created by a
* {@link SerializedFlowExecutionSnapshotFactory}.
*
* @see SerializedFlowExecutionSnapshotFactory
*
* @author Keith Donald
* @author Erwin Vervaet
*/
public class SerializedFlowExecutionSnapshot extends FlowExecutionSnapshot implements Externalizable {
private byte[] flowExecutionData;
private boolean compressed;
/**
* Default constructor necessary for {@link Externalizable} custom serialization semantics. Should not be called by
* application code.
*/
public SerializedFlowExecutionSnapshot() {
}
/**
* Creates a new serialized flow execution snapshot.
* @param flowExecution the flow execution
* @param compress whether or not to apply compression during snapshotting
*/
public SerializedFlowExecutionSnapshot(FlowExecution flowExecution, boolean compress)
throws SnapshotCreationException {
try {
flowExecutionData = serialize(flowExecution);
if (compress) {
flowExecutionData = compress(flowExecutionData);
}
} catch (NotSerializableException e) {
throw new SnapshotCreationException(flowExecution, "Could not serialize flow execution; "
+ "make sure all objects stored in flow or flash scope are serializable", e);
} catch (IOException e) {
throw new SnapshotCreationException(flowExecution,
"IOException thrown serializing flow execution -- this should not happen!", e);
}
this.compressed = compress;
}
/**
* Returns whether or not the flow execution data in this snapshot is compressed.
*/
public boolean isCompressed() {
return compressed;
}
/**
* Unmarshal the flow execution from this snapshot's data.
* @param classLoader the classloader to use to resolve types during execution deserialization
* @return the unmarashalled flow execution
* @throws SnapshotUnmarshalException
*/
public FlowExecution unmarshal(ClassLoader classLoader) throws SnapshotUnmarshalException {
try {
return deserialize(getFlowExecutionData(), classLoader);
} catch (IOException e) {
throw new SnapshotUnmarshalException(
"IOException thrown deserializing the flow execution stored in this snapshot -- this should not happen!",
e);
} catch (ClassNotFoundException e) {
throw new SnapshotUnmarshalException(
"ClassNotFoundException thrown deserializing the flow execution stored in this snapshot -- "
+ "This should not happen! Make sure there are no classloader issues. "
+ "For example, perhaps the Web Flow system is being loaded by a classloader "
+ "that is a parent of the classloader loading application classes?", e);
}
}
public boolean equals(Object o) {
if (!(o instanceof SerializedFlowExecutionSnapshot)) {
return false;
}
SerializedFlowExecutionSnapshot c = (SerializedFlowExecutionSnapshot) o;
return Arrays.equals(flowExecutionData, c.flowExecutionData);
}
public int hashCode() {
int hashCode = 0;
for (byte element : flowExecutionData) {
hashCode += element;
}
return hashCode;
}
// implementing Externalizable for custom serialization
public void writeExternal(ObjectOutput out) throws IOException {
// write out length first
out.writeInt(flowExecutionData.length);
// write out contents
out.write(flowExecutionData);
out.writeBoolean(compressed);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// read length of data array
int length = in.readInt();
flowExecutionData = new byte[length];
// read in contents in full
in.readFully(flowExecutionData);
compressed = in.readBoolean();
}
// subclassing hooks
/**
* Return the flow execution data in its raw byte[] form. Will decompress if necessary.
* @return the byte array
* @throws IOException a problem occured with decompression
*/
protected byte[] getFlowExecutionData() throws IOException {
if (isCompressed()) {
return decompress(flowExecutionData);
} else {
return flowExecutionData;
}
}
/**
* Internal helper method to serialize given flow execution. Override if a custom serialization method is used.
* @param flowExecution flow execution to serialize
* @return serialized flow flow execution data
* @throws IOException when something goes wrong during during serialization
*/
protected byte[] serialize(FlowExecution flowExecution) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
ObjectOutputStream oos = new ObjectOutputStream(baos);
try {
oos.writeObject(flowExecution);
oos.flush();
return baos.toByteArray();
} finally {
oos.close();
}
}
/**
* Internal helper method to deserialize given flow execution data. Override if a custom serialization method is
* used.
* @param data serialized flow flow execution data
* @param classLoader the class loader to use to resolve classes during deserialization
* @return deserialized flow execution
* @throws IOException when something goes wrong during deserialization
* @throws ClassNotFoundException when required classes cannot be loaded
*/
protected FlowExecution deserialize(byte[] data, ClassLoader classLoader) throws IOException,
ClassNotFoundException {
ObjectInputStream ois = new ConfigurableObjectInputStream(new ByteArrayInputStream(data), classLoader);
try {
return (FlowExecution) ois.readObject();
} finally {
ois.close();
}
}
/**
* Internal helper method to compress given flow execution data using GZIP compression. Override if custom
* compression is desired.
*/
protected byte[] compress(byte[] dataToCompress) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipos = new GZIPOutputStream(baos);
try {
gzipos.write(dataToCompress);
gzipos.flush();
} finally {
gzipos.close();
}
return baos.toByteArray();
}
/**
* Internal helper method to decompress given flow execution data using GZIP decompression. Override if custom
* decompression is desired.
*/
protected byte[] decompress(byte[] dataToDecompress) throws IOException {
GZIPInputStream gzipin = new GZIPInputStream(new ByteArrayInputStream(dataToDecompress));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
FileCopyUtils.copy(gzipin, baos);
} finally {
gzipin.close();
}
return baos.toByteArray();
}
private static class ConfigurableObjectInputStream extends ObjectInputStream {
/* Temporary workaround for SPR-???? */
private static final HashMap<String, Class<?>> PRIMITIVE_CLASSES = new HashMap<String, Class<?>>(8, 1.0F);
static {
PRIMITIVE_CLASSES.put("boolean", boolean.class);
PRIMITIVE_CLASSES.put("byte", byte.class);
PRIMITIVE_CLASSES.put("char", char.class);
PRIMITIVE_CLASSES.put("short", short.class);
PRIMITIVE_CLASSES.put("int", int.class);
PRIMITIVE_CLASSES.put("long", long.class);
PRIMITIVE_CLASSES.put("float", float.class);
PRIMITIVE_CLASSES.put("double", double.class);
PRIMITIVE_CLASSES.put("void", void.class);
}
private final ClassLoader classLoader;
public ConfigurableObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException {
super(in);
this.classLoader = classLoader;
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
try {
return ClassUtils.forName(desc.getName(), classLoader);
} catch (ClassNotFoundException ex) {
Class<?> rtn = PRIMITIVE_CLASSES.get(name);
if (rtn == null) {
throw ex;
}
return rtn;
}
}
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
ClassLoader nonPublicLoader = null;
boolean hasNonPublicInterface = false;
// define proxy in class loader of non-public interface(s), if any
Class<?>[] classObjs = new Class[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
Class<?> cl = ClassUtils.forName(interfaces[i], classLoader);
if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
if (hasNonPublicInterface) {
if (nonPublicLoader != cl.getClassLoader()) {
throw new IllegalAccessError("Conflicting non-public interface class loaders");
}
} else {
nonPublicLoader = cl.getClassLoader();
hasNonPublicInterface = true;
}
}
classObjs[i] = cl;
}
try {
return Proxy.getProxyClass(hasNonPublicInterface ? nonPublicLoader : classLoader, classObjs);
} catch (IllegalArgumentException e) {
throw new ClassNotFoundException(null, e);
}
}
}
}