blob: ac5d95a4625305e2b7a61b1a13c929848bb05893 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.axis2.context.externalize;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream.PutField;
import java.io.ObjectStreamConstants;
import java.io.Serializable;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A SafeObjectOutputStream provides extra mechanisms to ensure that
* objects can be safely serialized to the ObjectOutput.
*
* If an Object is written to a normal ObjectOutput, the ObjectOutput is left in
* an unknown state if a NotSerializableException occurs.
*
* The SafeObjectOutputStream does some additonal checking to ensure that the Object can
* be safely written. If the Object is suspicious, it is first written to a buffer to ensure
* that the underlying ObjectOutput is not corrupted.
*
* In addition, SafeObjectOutputStream provides extra methods to write containers of Objects.
* For example the writeMap object will write the key and value pairs that are can be serialized.
*
* @see SafeObjectInputStream
*
*/
public class SafeObjectOutputStream implements ObjectOutput,
ObjectStreamConstants, ExternalizeConstants {
private static final Log log = LogFactory.getLog(SafeObjectOutputStream.class);
private static final boolean isDebug = log.isDebugEnabled();
// Actual Stream
private ObjectOutput out = null;
// There are two ways to write out an object, a series of bytes or an Object.
// These flags are embedded in the stream so that the reader knows which form was used.
private static final boolean FORM_BYTE = false;
private static final boolean FORM_OBJECT = true;
// Temporary ObjectOutputStream for
MyOOS tempOOS = null;
// As a way to improve performance and reduce trace logging with
// extra exceptions, keep a table of classes that are not serializable
// and only log the first time it that the class is encountered in
// an NotSerializableException
// note that the Hashtable is synchronized by Java so we shouldn't need to
// do extra control over access to the table
public static final Hashtable notSerializableList = new Hashtable();
/**
* Add the SafeOutputStream if necessary.
* @param out Current ObjectOutput
* @return
* @throws IOException
*/
public static SafeObjectOutputStream install(ObjectOutput out) throws IOException {
if (out instanceof SafeObjectOutputStream) {
return (SafeObjectOutputStream) out;
}
return new SafeObjectOutputStream(out);
}
/**
* Intentionally private.
* Callers should use the install method to add the SafeObjectOutputStream
* into the stream.
* @param oo
* @throws IOException
*/
private SafeObjectOutputStream(ObjectOutput oo) throws IOException {
if (log.isDebugEnabled()) {
this.out = new DebugObjectOutputStream(oo);
} else {
this.out = oo;
}
}
// START DELEGATE METHODS
public void close() throws IOException {
if (tempOOS != null) {
tempOOS.close();
tempOOS = null;
}
out.close();
}
public void defaultWriteObject() throws IOException {
if (out instanceof ObjectOutputStream) {
((ObjectOutputStream)out).defaultWriteObject();
}
}
public boolean equals(Object o) {
return out.equals(o);
}
public void flush() throws IOException {
out.flush();
}
public int hashCode() {
return out.hashCode();
}
public PutField putFields() throws IOException {
if (out instanceof ObjectOutputStream) {
return ((ObjectOutputStream)out).putFields();
} else {
throw new IOException("This method is not supported.");
}
}
public void reset() throws IOException {
if (out instanceof ObjectOutputStream) {
((ObjectOutputStream)out).reset();
}
}
public String toString() {
return out.toString();
}
public void useProtocolVersion(int version) throws IOException {
if (out instanceof ObjectOutputStream) {
((ObjectOutputStream)out).useProtocolVersion(version);
}
}
public void write(byte[] buf, int off, int len) throws IOException {
out.write(buf, off, len);
}
public void write(byte[] buf) throws IOException {
out.write(buf);
}
public void write(int val) throws IOException {
out.write(val);
}
public void writeBoolean(boolean val) throws IOException {
out.writeBoolean(val);
}
public void writeByte(int val) throws IOException {
out.writeByte(val);
}
public void writeBytes(String str) throws IOException {
out.writeBytes(str);
}
public void writeChar(int val) throws IOException {
out.writeChar(val);
}
public void writeChars(String str) throws IOException {
out.writeChars(str);
}
public void writeDouble(double val) throws IOException {
out.writeDouble(val);
}
public void writeFields() throws IOException {
if (out instanceof ObjectOutputStream) {
((ObjectOutputStream)out).writeFields();
}
}
public void writeFloat(float val) throws IOException {
out.writeFloat(val);
}
public void writeInt(int val) throws IOException {
out.writeInt(val);
}
public void writeLong(long val) throws IOException {
out.writeLong(val);
}
public void writeObject(Object obj) throws IOException {
writeObject(obj, false); // Assume object is not safe
}
public void writeShort(int val) throws IOException {
out.writeShort(val);
}
public void writeUTF(String str) throws IOException {
out.writeUTF(str);
}
// END DELEGATE METHODS
/**
* Write a map
*
* FORMAT for null map
* EMPTY_OBJECT
*
* FORMAT for non-empty map
* ACTIVE_OBJECT
* for each contained key value pair
* writePair
* EMPTY_OBJECT (indicates end of the list
*
* @param ll
* @return
* @throws IOException
*/
public boolean writeMap(Map map) throws IOException {
if (map == null) {
out.writeBoolean(EMPTY_OBJECT);
return false;
} else {
out.writeBoolean(ACTIVE_OBJECT);
Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry entry = (Entry) it.next();
writePair(entry.getKey(), false, entry.getValue(), false);
} // Empty object indicates end of list
out.writeBoolean(EMPTY_OBJECT);
}
return true;
}
/**
* Write a list.
*
* FORMAT for null list
* EMPTY_OBJECT
*
* FORMAT for non-empty list
* ACTIVE_OBJECT
* for each contained object
* ACTOVE_OBJECT
* writeObject
* EMPTY_OBJECT (indicates end of the list
*
* @param ll
* @return
* @throws IOException
*/
public boolean writeList(List al) throws IOException {
if (al == null) {
out.writeBoolean(EMPTY_OBJECT);
return false;
} else {
out.writeBoolean(ACTIVE_OBJECT);
Iterator it = al.iterator();
while (it.hasNext()) {
Object value = it.next();
writeItem(value, false);
}
// Empty object indicates end of list
out.writeBoolean(EMPTY_OBJECT);
}
return true;
}
/**
* Writes an object to the stream.
* If the object is known (apriori) to be completely serializable it
* is "safe". Safe objects are written directly to the stream.
* Objects that are not known are to be safe are tested for safety and
* only written if they are deemed safe. Unsafe objects are not written.
* Note: The java.io.ObjectOutputStream is left in an unrecoverable state
* if any object written to it causes a serialization error. So please
* use the isSafe parameter wisely
*
* FORMAT for NULL Object
* EMPTY_OBJECT
*
* FORMAT for non-serializable Object
* EMPTY_OBJECT
*
* FORMAT for safe serializable Object
* ACTIVE_OBJECT
* FORM_OBJECT
* Object
*
* FORMAT for other serializable Object
* ACTIVE_OBJECT
* FORM_BYTE
* length of bytes
* bytes representing the object
*
* @param obj
* @param isSafe true if you know that object can be safely serialized. false if the
* object needs to be tested for serialization.
* @returns true if written
* @throws IOException
*/
private boolean writeObject(Object obj,
boolean isSafe) throws IOException {
if (isDebug) {
log.debug("Writing object:" + valueName(obj));
}
// Shortcut for null objects
if (obj == null) {
out.writeBoolean(EMPTY_OBJECT);
return false;
}
// Shortcut for non-serializable objects
if (!isSafe) {
if (!isSerializable(obj)) {
out.writeBoolean(EMPTY_OBJECT);
return false;
}
}
// If not safe, see if there are characteristics of the Object
// that guarantee that it can be safely serialized
// (for example Strings are always serializable)
if (!isSafe) {
isSafe = isSafeSerializable(obj);
}
if (isSafe) {
// Use object form
if (isDebug) {
log.debug(" write using object form");
}
out.writeBoolean(ACTIVE_OBJECT);
out.writeBoolean(FORM_OBJECT);
out.writeObject(obj);
} else {
// Fall-back to byte form
if (isDebug) {
log.debug(" write using byte form");
}
MyOOS tempOOS;
try {
tempOOS = writeTempOOS(obj);
} catch (IOException e) {
// Put a EMPTY object in the file
out.writeBoolean(EMPTY_OBJECT);
throw e;
}
if (tempOOS == null) {
out.writeBoolean(EMPTY_OBJECT);
return false;
}
out.writeBoolean(ACTIVE_OBJECT);
out.writeBoolean(FORM_BYTE);
tempOOS.write(out);
resetOnSuccess();
}
return true;
}
/**
* Writes pair of objects to the stream.
*
* If the objects are known (apriori) to be completely serializable they
* are "safe". Safe objects are written directly to the stream.
* Objects that are not known are to be safe are tested for safety and
* only written if they are deemed safe. Unsafe objects are not written.
* Note: The java.io.ObjectOutputStream is left in an unrecoverable state
* if any object written to it causes a serialization error. So please
* use the isSafe parameter wisely
*
*
* FORMAT for non-serializable key/value pair
* nothing is written
*
* FORMAT for safe serializable key/value pair
* ACTIVE_OBJECT
* FORM_OBJECT
* Object
*
* FORMAT for other serializable key/value pair
* ACTIVE_OBJECT
* FORM_BYTE
* length of bytes
* bytes representing the object
*
* @param obj1
* @param isSafe1 true if you know that object can be safely serialized. false if the
* object needs to be tested for serialization.
* @param obj2
* @param isSafe2 true if you know that object can be safely serialized. false if the
* object needs to be tested for serialization.
* @returns true if both are written to the stream
* @throws IOException
*/
public boolean writePair(Object obj1,
boolean isSafe1,
Object obj2,
boolean isSafe2) throws IOException {
if (isDebug) {
log.debug("Writing key=" + valueName(obj1) +
" value="+valueName(obj2));
}
// Shortcut for non-serializable objects
if ((!isSafe1 && !isSerializable(obj1)) ||
(!isSafe2 && !isSerializable(obj2))) {
return false;
}
boolean isSafe = (isSafe1 || isSafeSerializable(obj1)) &&
(isSafe2 || isSafeSerializable(obj2));
if (isSafe) {
if (isDebug) {
log.debug(" write using object form");
}
out.writeBoolean(ACTIVE_OBJECT);
out.writeBoolean(FORM_OBJECT);
out.writeObject(obj1);
out.writeObject(obj2);
} else {
if (isDebug) {
log.debug(" write using byte form");
}
MyOOS tempOOS = writeTempOOS(obj1, obj2);
if (tempOOS == null) {
return false;
}
out.writeBoolean(ACTIVE_OBJECT);
out.writeBoolean(FORM_BYTE);
tempOOS.write(out);
resetOnSuccess();
}
return true;
}
/**
* Writes pair of objects to the stream.
*
* If the objects are known (apriori) to be completely serializable they
* are "safe". Safe objects are written directly to the stream.
* Objects that are not known are to be safe are tested for safety and
* only written if they are deemed safe. Unsafe objects are not written.
* Note: The java.io.ObjectOutputStream is left in an unrecoverable state
* if any object written to it causes a serialization error. So please
* use the isSafe parameter wisely
*
*
* FORMAT for non-serializable key/value pair
* nothing is written
*
* FORMAT for safe serializable key/value pair
* ACTIVE_OBJECT
* FORM_OBJECT
* Object
*
* FORMAT for other serializable key/value pair
* ACTIVE_OBJECT
* FORM_BYTE
* length of bytes
* bytes representing the object
*
* @param obj1
* @param isSafe1 true if you know that object can be safely serialized. false if the
* object needs to be tested for serialization.
* @param obj2
* @param isSafe2 true if you know that object can be safely serialized. false if the
* object needs to be tested for serialization.
* @returns true if both are written to the stream
* @throws IOException
*/
public boolean writeItem(Object obj,
boolean isSafe) throws IOException {
if (isDebug) {
log.debug("Writing obj=" + valueName(obj));
}
// Shortcut for non-serializable objects
if (!isSafe && !isSerializable(obj)) {
return false;
}
isSafe = (isSafe || isSafeSerializable(obj));
if (isSafe) {
if (isDebug) {
log.debug(" write using object form");
}
out.writeBoolean(ACTIVE_OBJECT);
out.writeBoolean(FORM_OBJECT);
out.writeObject(obj);
} else {
if (isDebug) {
log.debug(" write using byte form");
}
MyOOS tempOOS;
try {
tempOOS = writeTempOOS(obj);
} catch (RuntimeException e) {
return false;
}
if (tempOOS == null) {
return false;
}
out.writeBoolean(ACTIVE_OBJECT);
out.writeBoolean(FORM_BYTE);
tempOOS.write(out);
resetOnSuccess();
}
return true;
}
/**
* Does a quick check of the implemented interfaces to ensure that this
* object is serializable
* @return true if the object is marked as Serializable
*/
private static boolean isSerializable(Object obj) {
boolean isSerializable = (obj == null) || obj instanceof Serializable;
if (!isSerializable) {
markNotSerializable(obj);
}
return isSerializable;
}
/**
* Does a quick check of the implemented class is safe to serialize without
* buffering.
* @return true if the object is marked as safe.
*/
private static boolean isSafeSerializable(Object obj) {
boolean isSafeSerializable = (obj == null) ||
obj instanceof SafeSerializable ||
obj instanceof String ||
obj instanceof Integer ||
obj instanceof Boolean ||
obj instanceof Long;
return isSafeSerializable;
}
/**
* Write the object to a temporary ObjectOutput
* @param obj
* @return ObjectOutput if successful
*/
private MyOOS writeTempOOS(Object obj) throws IOException {
MyOOS oos = null;
try {
oos = getTempOOS();
oos.writeObject(obj);
oos.flush();
} catch (NotSerializableException nse2) {
markNotSerializable(obj);
if (oos != null) {
resetOnFailure();
oos = null;
}
throw nse2;
} catch (IOException e) {
if (oos != null) {
resetOnFailure();
oos = null;
}
throw e;
} catch (RuntimeException e) {
if (oos != null) {
resetOnFailure();
oos = null;
}
throw e;
}
return oos;
}
/**
* Write the objects to a temporary ObjectOutput
* @param obj1
* @param obj2
* @return ObjectOutput if successful
*/
private MyOOS writeTempOOS(Object obj1, Object obj2) throws IOException {
MyOOS oos = null;
boolean first = true;
try {
oos = getTempOOS();
oos.writeObject(obj1);
first = false;
oos.writeObject(obj2);
oos.flush();
} catch (NotSerializableException nse2) {
// This is okay and expected in some cases.
// Log the error and continue
markNotSerializable((first) ? obj1 :obj2);
if (oos != null) {
resetOnFailure();
oos = null;
}
} catch (IOException e) {
if (oos != null) {
resetOnFailure();
oos = null;
}
throw e;
} catch (RuntimeException e) {
if (oos != null) {
resetOnFailure();
oos = null;
}
throw e;
}
return oos;
}
/**
* Get or create a temporary ObjectOutputStream
* @return MyOOS
* @throws IOException
*/
private MyOOS getTempOOS() throws IOException {
if (tempOOS == null) {
tempOOS = new MyOOS(new MyBAOS());
}
return tempOOS;
}
/**
* If a failure occurs, reset the temporary ObjectOutputStream
*/
private void resetOnFailure() throws IOException {
if (tempOOS != null) {
tempOOS.close();
tempOOS = null; // The ObjectOutput is in an unknown state and thus discarded
}
}
/**
* Reset the temporary ObjectOutputStream
* @throws IOException
*/
private void resetOnSuccess() throws IOException {
tempOOS.reset();
}
private static void markNotSerializable(Object obj) {
if (!isDebug) {
return;
}
if (obj != null) {
String name = obj.getClass().getName();
Object value = notSerializableList.get(name);
if (value == null) {
notSerializableList.put(name, name);
if (log.isTraceEnabled()) {
log.trace("***NotSerializableException*** [" + name + "]");
}
}
}
}
private String valueName(Object obj) {
if (obj == null) {
return "null";
} else if (obj instanceof String) {
return (String) obj;
} else {
return "Object of class = " + obj.getClass().getName();
}
}
/**
* MyBAOS is a ByteArrayOutputStream with a few additions.
*
*/
static class MyBAOS extends ByteArrayOutputStream {
/**
* Return direct access to the buffer without creating a copy of the byte[]
* @return buf
*/
public byte[] getBytes() {
return buf;
}
/**
* Reset to a specific index in the buffer
* @param count
*/
public void reset(int count) {
this.count = count;
}
}
/**
* MyOOS is an ObjectOutputStream with a few performant additions.
*
*/
static class MyOOS extends ObjectOutputStream {
MyBAOS baos;
int dataOffset;
MyOOS(MyBAOS baos) throws IOException {
super(baos);
super.flush();
this.baos = baos;
// Capture the data offset
// (the location where data starts..which is after header information)
dataOffset = baos.size();
}
/**
* Override the reset so that we can reset to the data offset.
*/
public void reset() throws IOException {
super.reset();
// Reset the byte stream to the position past the headers
baos.reset(dataOffset);
}
/**
* Write the contents of MyOOS to the indicated ObjectOutput.
* Note that this direct write avoids any byte[] buffer creation
* @param out
* @throws IOException
*/
public void write(ObjectOutput out) throws IOException {
out.flush();
// out.writeObject(out.toByteArray());
out.writeInt(baos.size());
out.write(baos.getBytes(),0,baos.size());
}
}
}