/* | |
* 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()); | |
} | |
} | |
} |