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