Updated Branches: refs/heads/wicket-1.5.x 19d06d27c -> 486b7eb88
WICKET-4812 Make SerializationChecker easier for extending so custom checks can be added to it Rename ObjectChecker to CheckingObjectOutputStream to make it more clear that it is not an implementation of IObjectChecker. Simplify JavaSerializer to use CheckingObjectOutputStream directly without another ObjectOutputStream as adapter. Project: http://git-wip-us.apache.org/repos/asf/wicket/repo Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/486b7eb8 Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/486b7eb8 Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/486b7eb8 Branch: refs/heads/wicket-1.5.x Commit: 486b7eb88df5fa11bb7892f6e135fb1b62c4a6a3 Parents: 19d06d2 Author: Martin Tzvetanov Grigorov <mgrigo...@apache.org> Authored: Wed Oct 17 11:17:12 2012 +0200 Committer: Martin Tzvetanov Grigorov <mgrigo...@apache.org> Committed: Wed Oct 17 11:17:12 2012 +0200 ---------------------------------------------------------------------- .../wicket/serialize/java/JavaSerializer.java | 25 +- .../apache/wicket/util/io/SerializableChecker.java | 27 +- .../checker/CheckingObjectOutputStream.java | 723 +++++++++++++++ .../wicket/util/objects/checker/ObjectChecker.java | 709 -------------- .../wicket/util/io/SerializableCheckerTest.java | 6 +- 5 files changed, 764 insertions(+), 726 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/wicket/blob/486b7eb8/wicket-core/src/main/java/org/apache/wicket/serialize/java/JavaSerializer.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/serialize/java/JavaSerializer.java b/wicket-core/src/main/java/org/apache/wicket/serialize/java/JavaSerializer.java index e37666b..7eebf32 100644 --- a/wicket-core/src/main/java/org/apache/wicket/serialize/java/JavaSerializer.java +++ b/wicket-core/src/main/java/org/apache/wicket/serialize/java/JavaSerializer.java @@ -34,6 +34,7 @@ import org.apache.wicket.serialize.ISerializer; import org.apache.wicket.settings.IApplicationSettings; import org.apache.wicket.util.io.IOUtils; import org.apache.wicket.util.io.SerializableChecker; +import org.apache.wicket.util.objects.checker.CheckingObjectOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,8 +44,6 @@ import org.slf4j.LoggerFactory; * * Requires the application key to enable serialization and deserialisation outside thread in which * application thread local is set - * - * @see IObjectStreamFactory */ public class JavaSerializer implements ISerializer { @@ -171,7 +170,7 @@ public class JavaSerializer implements ISerializer */ protected ObjectOutputStream newObjectOutputStream(OutputStream out) throws IOException { - return new CheckerObjectOutputStream(out); + return new SerializationCheckingObjectOutputStream(out); } /** @@ -231,15 +230,23 @@ public class JavaSerializer implements ISerializer } /** * Write objects to the wrapped output stream and log a meaningful message for serialization - * problems + * problems. + * + * <p> + * Note: the checking functionality is used only if the serialization fails with NotSerializableException. + * This is done so to save some CPU time to make the checks for no reason. + * </p> */ - private static class CheckerObjectOutputStream extends ObjectOutputStream + private static class SerializationCheckingObjectOutputStream extends ObjectOutputStream { + private final OutputStream outputStream; + private final ObjectOutputStream oos; - public CheckerObjectOutputStream(OutputStream out) throws IOException + private SerializationCheckingObjectOutputStream(OutputStream outputStream) throws IOException { - oos = new ObjectOutputStream(out); + this.outputStream = outputStream; + oos = new ObjectOutputStream(outputStream); } @Override @@ -251,11 +258,11 @@ public class JavaSerializer implements ISerializer } catch (NotSerializableException nsx) { - if (SerializableChecker.isAvailable()) + if (CheckingObjectOutputStream.isAvailable()) { // trigger serialization again, but this time gather // some more info - new SerializableChecker(nsx).writeObject(obj); + new SerializableChecker(outputStream, nsx).writeObject(obj); // if we get here, we didn't fail, while we // should; throw nsx; http://git-wip-us.apache.org/repos/asf/wicket/blob/486b7eb8/wicket-core/src/main/java/org/apache/wicket/util/io/SerializableChecker.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/util/io/SerializableChecker.java b/wicket-core/src/main/java/org/apache/wicket/util/io/SerializableChecker.java index 8353bd9..a6eff20 100644 --- a/wicket-core/src/main/java/org/apache/wicket/util/io/SerializableChecker.java +++ b/wicket-core/src/main/java/org/apache/wicket/util/io/SerializableChecker.java @@ -18,25 +18,27 @@ package org.apache.wicket.util.io; import java.io.IOException; import java.io.NotSerializableException; +import java.io.OutputStream; import java.io.Serializable; import java.lang.reflect.Proxy; import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.util.io.ByteArrayOutputStream; import org.apache.wicket.util.objects.checker.AbstractObjectChecker; -import org.apache.wicket.util.objects.checker.ObjectChecker; +import org.apache.wicket.util.objects.checker.CheckingObjectOutputStream; /** * Utility class that analyzes objects for non-serializable nodes. Construct, then call * {@link #writeObject(Object)} with the object you want to check. When a non-serializable object is - * found, a {@link WicketNotSerializableException} is thrown with a message that shows the trace up + * found, a {@link ObjectCheckException} is thrown with a message that shows the trace up * to the not-serializable object. The exception is thrown for the first non-serializable instance * it encounters, so multiple problems will not be shown. * * @author eelcohillenius * @author Al Maw */ -public class SerializableChecker extends ObjectChecker +public class SerializableChecker extends CheckingObjectOutputStream { /** * Exception that is thrown when a non-serializable object was found. @@ -112,7 +114,22 @@ public class SerializableChecker extends ObjectChecker */ public SerializableChecker(NotSerializableException exception) throws IOException { - super(new ObjectSerializationChecker(exception)); + this(new ByteArrayOutputStream(), exception); + } + + /** + * Constructor. + * + * @param exception + * exception that should be set as the cause when throwing a new exception + * @param outputStream + * the output stream where the serialized object will be written upon successful check + * + * @throws IOException + */ + public SerializableChecker(final OutputStream outputStream, NotSerializableException exception) throws IOException + { + super(outputStream, new ObjectSerializationChecker(exception)); } /** @@ -125,6 +142,6 @@ public class SerializableChecker extends ObjectChecker @Deprecated public static boolean isAvailable() { - return ObjectChecker.isAvailable(); + return CheckingObjectOutputStream.isAvailable(); } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/wicket/blob/486b7eb8/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/CheckingObjectOutputStream.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/CheckingObjectOutputStream.java b/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/CheckingObjectOutputStream.java new file mode 100644 index 0000000..7071980 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/CheckingObjectOutputStream.java @@ -0,0 +1,723 @@ +/* + * 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.wicket.util.objects.checker; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.ObjectStreamField; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Date; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +import org.apache.wicket.Component; +import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.util.lang.Classes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Checks an object tree during serialization for wrong state by delegating the work + * to the used {@link IObjectChecker IObjectChecker}s. + * <p> + * As this class depends heavily on JDK's serialization internals using introspection, analyzing may + * not be possible, for instance when the runtime environment does not have sufficient rights to set + * fields accessible that would otherwise be hidden. You should call + * {@link CheckingObjectOutputStream#isAvailable()} to see whether this class can operate properly. + * + * + * An ObjectOutputStream that uses {@link IObjectChecker IObjectChecker}s to check the + * state of the object before serializing it. If the checker returns + * {@link org.apache.wicket.util.objects.checker.IObjectChecker.Result.Status#FAILURE} + * then the serialization process is stopped and the error is logged. + * </p> + */ +public class CheckingObjectOutputStream extends ObjectOutputStream +{ + private static final Logger log = LoggerFactory.getLogger(CheckingObjectOutputStream.class); + + public static class ObjectCheckException extends WicketRuntimeException + { + public ObjectCheckException(String message, Throwable cause) + { + super(message, cause); + } + } + + /** + * Does absolutely nothing. + */ + private static class NoopOutputStream extends OutputStream + { + @Override + public void close() + { + } + + @Override + public void flush() + { + } + + @Override + public void write(byte[] b) + { + } + + @Override + public void write(byte[] b, int i, int l) + { + } + + @Override + public void write(int b) + { + } + } + + private static abstract class ObjectOutputAdaptor implements ObjectOutput + { + public void close() throws IOException + { + } + + public void flush() throws IOException + { + } + + public void write(byte[] b) throws IOException + { + } + + public void write(byte[] b, int off, int len) throws IOException + { + } + + public void write(int b) throws IOException + { + } + + public void writeBoolean(boolean v) throws IOException + { + } + + public void writeByte(int v) throws IOException + { + } + + public void writeBytes(String s) throws IOException + { + } + + public void writeChar(int v) throws IOException + { + } + + public void writeChars(String s) throws IOException + { + } + + public void writeDouble(double v) throws IOException + { + } + + public void writeFloat(float v) throws IOException + { + } + + public void writeInt(int v) throws IOException + { + } + + public void writeLong(long v) throws IOException + { + } + + public void writeShort(int v) throws IOException + { + } + + public void writeUTF(String str) throws IOException + { + } + } + + /** Holds information about the field and the resulting object being traced. */ + private static final class TraceSlot + { + private final String fieldDescription; + + private final Object object; + + TraceSlot(Object object, String fieldDescription) + { + this.object = object; + this.fieldDescription = fieldDescription; + } + + @Override + public String toString() + { + return object.getClass() + " - " + fieldDescription; + } + } + + private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new NoopOutputStream(); + + /** Whether we can execute the tests. If false, check will just return. */ + private static boolean available = true; + + // this hack - accessing the serialization API through introspection - is + // the only way to use Java serialization for our purposes without writing + // the whole thing from scratch (and even then, it would be limited). This + // way of working is of course fragile for internal API changes, but as we + // do an extra check on availability and we report when we can't use this + // introspection fu, we'll find out soon enough and clients on this class + // can fall back on Java's default exception for serialization errors (which + // sucks and is the main reason for this attempt). + private static Method LOOKUP_METHOD; + + private static Method GET_CLASS_DATA_LAYOUT_METHOD; + + private static Method GET_NUM_OBJ_FIELDS_METHOD; + + private static Method GET_OBJ_FIELD_VALUES_METHOD; + + private static Method GET_FIELD_METHOD; + + private static Method HAS_WRITE_REPLACE_METHOD_METHOD; + + private static Method INVOKE_WRITE_REPLACE_METHOD; + + static + { + try + { + LOOKUP_METHOD = ObjectStreamClass.class.getDeclaredMethod("lookup", new Class[] { + Class.class, Boolean.TYPE }); + LOOKUP_METHOD.setAccessible(true); + + GET_CLASS_DATA_LAYOUT_METHOD = ObjectStreamClass.class.getDeclaredMethod( + "getClassDataLayout", (Class[])null); + GET_CLASS_DATA_LAYOUT_METHOD.setAccessible(true); + + GET_NUM_OBJ_FIELDS_METHOD = ObjectStreamClass.class.getDeclaredMethod( + "getNumObjFields", (Class[])null); + GET_NUM_OBJ_FIELDS_METHOD.setAccessible(true); + + GET_OBJ_FIELD_VALUES_METHOD = ObjectStreamClass.class.getDeclaredMethod( + "getObjFieldValues", new Class[] { Object.class, Object[].class }); + GET_OBJ_FIELD_VALUES_METHOD.setAccessible(true); + + GET_FIELD_METHOD = ObjectStreamField.class.getDeclaredMethod("getField", (Class[])null); + GET_FIELD_METHOD.setAccessible(true); + + HAS_WRITE_REPLACE_METHOD_METHOD = ObjectStreamClass.class.getDeclaredMethod( + "hasWriteReplaceMethod", (Class[])null); + HAS_WRITE_REPLACE_METHOD_METHOD.setAccessible(true); + + INVOKE_WRITE_REPLACE_METHOD = ObjectStreamClass.class.getDeclaredMethod( + "invokeWriteReplace", new Class[] { Object.class }); + INVOKE_WRITE_REPLACE_METHOD.setAccessible(true); + } + catch (Exception e) + { + log.warn("SerializableChecker not available", e); + available = false; + } + } + + private final IObjectChecker[] checkers; + + /** + * Gets whether we can execute the tests. If false, calling {@link #check(Object)} will just + * return and you are advised to rely on the {@link java.io.NotSerializableException}. Clients are + * advised to call this method prior to calling the check method. + * + * @return whether security settings and underlying API etc allow for accessing the + * serialization API using introspection + */ + public static boolean isAvailable() + { + return available; + } + + /** + * The output stream where the serialized object will be written upon successful check + */ + private final ObjectOutputStream out; + + /** object stack with the trace path. */ + private final LinkedList<TraceSlot> traceStack = new LinkedList<TraceSlot>(); + + /** set for checking circular references. */ + private final Map<Object, Object> checked = new IdentityHashMap<Object, Object>(); + + /** string stack with current names pushed. */ + private final LinkedList<CharSequence> nameStack = new LinkedList<CharSequence>(); + + /** root object being analyzed. */ + private Object root; + + /** set of classes that had no writeObject methods at lookup (to avoid repeated checking) */ + private final Set<Class<?>> writeObjectMethodMissing = new HashSet<Class<?>>(); + + /** current simple field name. */ + private CharSequence simpleName = ""; + + /** current full field description. */ + private String fieldDescription; + + private final Stack<Object> stack = new Stack<Object>(); + + /** + * Constructor. + * + * @param outputStream + * the output stream where the serialized object will be written upon successful check + * @param checkers + * the {@link IObjectChecker checkers} that will actually check the objects + * @throws IOException + * @throws SecurityException + */ + public CheckingObjectOutputStream(final OutputStream outputStream, final IObjectChecker... checkers) throws IOException, SecurityException + { + this.out = new ObjectOutputStream(outputStream); + this.checkers = checkers; + } + + private void check(Object obj) + { + if (obj == null) + { + return; + } + + try + { + if (stack.contains(obj)) + { + return; + } + } + catch (RuntimeException e) + { + log.warn("Wasn't possible to check the object '{}' possible due an problematic " + + "implementation of equals method", obj.getClass()); + /* + * Can't check if this obj were in stack, giving up because we don't want to throw an + * invaluable exception to user. The main goal of this checker is to find non + * serializable data + */ + return; + } + + stack.push(obj); + try + { + internalCheck(obj); + } + finally + { + stack.pop(); + } + } + + private void internalCheck(Object obj) + { + if (obj == null) + { + return; + } + + Class<?> cls = obj.getClass(); + nameStack.add(simpleName); + traceStack.add(new TraceSlot(obj, fieldDescription)); + + for (IObjectChecker checker : checkers) + { + IObjectChecker.Result result = checker.check(obj); + if (result.status == IObjectChecker.Result.Status.FAILURE) + { + String prettyPrintMessage = toPrettyPrintedStack(Classes.name(cls)); + String exceptionMessage = result.reason + '\n' + prettyPrintMessage; + throw new ObjectCheckException(exceptionMessage, result.cause); + } + } + + ObjectStreamClass desc; + for (;;) + { + try + { + desc = (ObjectStreamClass)LOOKUP_METHOD.invoke(null, cls, Boolean.TRUE); + Class<?> repCl; + if (!(Boolean)HAS_WRITE_REPLACE_METHOD_METHOD.invoke(desc, (Object[])null) || + (obj = INVOKE_WRITE_REPLACE_METHOD.invoke(desc, obj)) == null || + (repCl = obj.getClass()) == cls) + { + break; + } + cls = repCl; + } + catch (IllegalAccessException e) + { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) + { + throw new RuntimeException(e); + } + } + + if (cls.isPrimitive()) + { + // skip + } + else if (cls.isArray()) + { + checked.put(obj, null); + Class<?> ccl = cls.getComponentType(); + if (!(ccl.isPrimitive())) + { + Object[] objs = (Object[])obj; + for (int i = 0; i < objs.length; i++) + { + CharSequence arrayPos = new StringBuilder(4).append('[').append(i).append(']'); + simpleName = arrayPos; + fieldDescription += arrayPos; + check(objs[i]); + } + } + } + else if (obj instanceof Externalizable && (!Proxy.isProxyClass(cls))) + { + Externalizable extObj = (Externalizable)obj; + try + { + extObj.writeExternal(new ObjectOutputAdaptor() + { + private int count = 0; + + public void writeObject(Object streamObj) throws IOException + { + // Check for circular reference. + if (checked.containsKey(streamObj)) + { + return; + } + + checked.put(streamObj, null); + CharSequence arrayPos = new StringBuilder(10).append("[write:").append(count++).append(']'); + simpleName = arrayPos; + fieldDescription += arrayPos; + + check(streamObj); + } + }); + } + catch (Exception e) + { + if (e instanceof ObjectCheckException) + { + throw (ObjectCheckException)e; + } + log.warn("Error delegating to Externalizable : {}, path: {}", e.getMessage(), currentPath()); + } + } + else + { + Method writeObjectMethod = null; + if (writeObjectMethodMissing.contains(cls) == false) + { + try + { + writeObjectMethod = cls.getDeclaredMethod("writeObject", + new Class[] { java.io.ObjectOutputStream.class }); + } + catch (SecurityException e) + { + // we can't access / set accessible to true + writeObjectMethodMissing.add(cls); + } + catch (NoSuchMethodException e) + { + // cls doesn't have that method + writeObjectMethodMissing.add(cls); + } + } + + final Object original = obj; + if (writeObjectMethod != null) + { + class InterceptingObjectOutputStream extends ObjectOutputStream + { + private int counter; + + InterceptingObjectOutputStream() throws IOException + { + super(DUMMY_OUTPUT_STREAM); + enableReplaceObject(true); + } + + @Override + protected Object replaceObject(Object streamObj) throws IOException + { + if (streamObj == original) + { + return streamObj; + } + + counter++; + // Check for circular reference. + if (checked.containsKey(streamObj)) + { + return null; + } + + checked.put(streamObj, null); + CharSequence arrayPos = new StringBuilder(10).append("[write:").append(counter).append(']'); + simpleName = arrayPos; + fieldDescription += arrayPos; + check(streamObj); + return streamObj; + } + } + try + { + InterceptingObjectOutputStream ioos = new InterceptingObjectOutputStream(); + ioos.writeObject(obj); + } + catch (Exception e) + { + if (e instanceof ObjectCheckException) + { + throw (ObjectCheckException)e; + } + log.warn("error delegating to writeObject : {}, path: {}", e.getMessage(), currentPath()); + } + } + else + { + Object[] slots; + try + { + slots = (Object[])GET_CLASS_DATA_LAYOUT_METHOD.invoke(desc, (Object[])null); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + for (Object slot : slots) + { + ObjectStreamClass slotDesc; + try + { + Field descField = slot.getClass().getDeclaredField("desc"); + descField.setAccessible(true); + slotDesc = (ObjectStreamClass)descField.get(slot); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + checked.put(obj, null); + checkFields(obj, slotDesc); + } + } + } + + traceStack.removeLast(); + nameStack.removeLast(); + } + + private void checkFields(Object obj, ObjectStreamClass desc) + { + int numFields; + try + { + numFields = (Integer)GET_NUM_OBJ_FIELDS_METHOD.invoke(desc, (Object[])null); + } + catch (IllegalAccessException e) + { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) + { + throw new RuntimeException(e); + } + + if (numFields > 0) + { + int numPrimFields; + ObjectStreamField[] fields = desc.getFields(); + Object[] objVals = new Object[numFields]; + numPrimFields = fields.length - objVals.length; + try + { + GET_OBJ_FIELD_VALUES_METHOD.invoke(desc, obj, objVals); + } + catch (IllegalAccessException e) + { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) + { + throw new RuntimeException(e); + } + for (int i = 0; i < objVals.length; i++) + { + if (objVals[i] instanceof String || objVals[i] instanceof Number || + objVals[i] instanceof Date || objVals[i] instanceof Boolean || + objVals[i] instanceof Class) + { + // filter out common cases + continue; + } + + // Check for circular reference. + if (checked.containsKey(objVals[i])) + { + continue; + } + + ObjectStreamField fieldDesc = fields[numPrimFields + i]; + Field field; + try + { + field = (Field)GET_FIELD_METHOD.invoke(fieldDesc, (Object[])null); + } + catch (IllegalAccessException e) + { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) + { + throw new RuntimeException(e); + } + + simpleName = field.getName(); + fieldDescription = field.toString(); + check(objVals[i]); + } + } + } + + /** + * @return name from root to current node concatenated with slashes + */ + private StringBuilder currentPath() + { + StringBuilder b = new StringBuilder(); + for (Iterator<CharSequence> it = nameStack.iterator(); it.hasNext();) + { + b.append(it.next()); + if (it.hasNext()) + { + b.append('/'); + } + } + return b; + } + + /** + * Dump with indentation. + * + * @param type + * the type that couldn't be serialized + * @return A very pretty dump + */ + protected final String toPrettyPrintedStack(String type) + { + StringBuilder result = new StringBuilder(512); + StringBuilder spaces = new StringBuilder(32); + result.append("A problem occurred while checking object with type: "); + result.append(type); + result.append("\nField hierarchy is:"); + for (TraceSlot slot : traceStack) + { + spaces.append(' ').append(' '); + result.append('\n').append(spaces).append(slot.fieldDescription); + result.append(" [class=").append(Classes.name(slot.object.getClass())); + if (slot.object instanceof Component) + { + Component component = (Component)slot.object; + result.append(", path=").append(component.getPath()); + } + result.append(']'); + } + result.append(" <----- field that is causing the problem"); + return result.toString(); + } + + /** + * @see java.io.ObjectOutputStream#writeObjectOverride(java.lang.Object) + */ + @Override + protected final void writeObjectOverride(Object obj) throws IOException + { + if (!available) + { + return; + } + root = obj; + if (fieldDescription == null) + { + fieldDescription = (root instanceof Component) ? ((Component)root).getPath() : ""; + } + + check(root); + out.writeObject(obj); + } + + /** + * @see java.io.ObjectOutputStream#reset() + */ + @Override + public void reset() throws IOException + { + root = null; + checked.clear(); + fieldDescription = null; + simpleName = null; + traceStack.clear(); + nameStack.clear(); + writeObjectMethodMissing.clear(); + } + + @Override + public void close() throws IOException + { + // do not call super.close() because SerializableChecker uses ObjectOutputStream's no-arg constructor + + // just null-ify the declared members + reset(); + } + +} http://git-wip-us.apache.org/repos/asf/wicket/blob/486b7eb8/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/ObjectChecker.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/ObjectChecker.java b/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/ObjectChecker.java deleted file mode 100644 index 3109981..0000000 --- a/wicket-core/src/main/java/org/apache/wicket/util/objects/checker/ObjectChecker.java +++ /dev/null @@ -1,709 +0,0 @@ -/* - * 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.wicket.util.objects.checker; - -import java.io.Externalizable; -import java.io.IOException; -import java.io.ObjectOutput; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamClass; -import java.io.ObjectStreamField; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Date; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; -import java.util.Set; -import java.util.Stack; - -import org.apache.wicket.Component; -import org.apache.wicket.WicketRuntimeException; -import org.apache.wicket.util.lang.Classes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Checks an object tree during serialization for wrong state by delegating the work - * to the used {@link IObjectChecker IObjectChecker}s. - * <p> - * As this class depends heavily on JDK's serialization internals using introspection, analyzing may - * not be possible, for instance when the runtime environment does not have sufficient rights to set - * fields accessible that would otherwise be hidden. You should call - * {@link ObjectChecker#isAvailable()} to see whether this class can operate properly. - * </p> - */ -public class ObjectChecker extends ObjectOutputStream -{ - private static final Logger log = LoggerFactory.getLogger(ObjectChecker.class); - - public static class ObjectCheckException extends WicketRuntimeException - { - public ObjectCheckException(String message, Throwable cause) - { - super(message, cause); - } - } - - /** - * Does absolutely nothing. - */ - private static class NoopOutputStream extends OutputStream - { - @Override - public void close() - { - } - - @Override - public void flush() - { - } - - @Override - public void write(byte[] b) - { - } - - @Override - public void write(byte[] b, int i, int l) - { - } - - @Override - public void write(int b) - { - } - } - - private static abstract class ObjectOutputAdaptor implements ObjectOutput - { - - public void close() throws IOException - { - } - - public void flush() throws IOException - { - } - - public void write(byte[] b) throws IOException - { - } - - public void write(byte[] b, int off, int len) throws IOException - { - } - - public void write(int b) throws IOException - { - } - - public void writeBoolean(boolean v) throws IOException - { - } - - public void writeByte(int v) throws IOException - { - } - - public void writeBytes(String s) throws IOException - { - } - - public void writeChar(int v) throws IOException - { - } - - public void writeChars(String s) throws IOException - { - } - - public void writeDouble(double v) throws IOException - { - } - - public void writeFloat(float v) throws IOException - { - } - - public void writeInt(int v) throws IOException - { - } - - public void writeLong(long v) throws IOException - { - } - - public void writeShort(int v) throws IOException - { - } - - public void writeUTF(String str) throws IOException - { - } - } - - /** Holds information about the field and the resulting object being traced. */ - private static final class TraceSlot - { - private final String fieldDescription; - - private final Object object; - - TraceSlot(Object object, String fieldDescription) - { - this.object = object; - this.fieldDescription = fieldDescription; - } - - @Override - public String toString() - { - return object.getClass() + " - " + fieldDescription; - } - } - - private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new NoopOutputStream(); - - /** Whether we can execute the tests. If false, check will just return. */ - private static boolean available = true; - - // this hack - accessing the serialization API through introspection - is - // the only way to use Java serialization for our purposes without writing - // the whole thing from scratch (and even then, it would be limited). This - // way of working is of course fragile for internal API changes, but as we - // do an extra check on availability and we report when we can't use this - // introspection fu, we'll find out soon enough and clients on this class - // can fall back on Java's default exception for serialization errors (which - // sucks and is the main reason for this attempt). - private static Method LOOKUP_METHOD; - - private static Method GET_CLASS_DATA_LAYOUT_METHOD; - - private static Method GET_NUM_OBJ_FIELDS_METHOD; - - private static Method GET_OBJ_FIELD_VALUES_METHOD; - - private static Method GET_FIELD_METHOD; - - private static Method HAS_WRITE_REPLACE_METHOD_METHOD; - - private static Method INVOKE_WRITE_REPLACE_METHOD; - - static - { - try - { - LOOKUP_METHOD = ObjectStreamClass.class.getDeclaredMethod("lookup", new Class[] { - Class.class, Boolean.TYPE }); - LOOKUP_METHOD.setAccessible(true); - - GET_CLASS_DATA_LAYOUT_METHOD = ObjectStreamClass.class.getDeclaredMethod( - "getClassDataLayout", (Class[])null); - GET_CLASS_DATA_LAYOUT_METHOD.setAccessible(true); - - GET_NUM_OBJ_FIELDS_METHOD = ObjectStreamClass.class.getDeclaredMethod( - "getNumObjFields", (Class[])null); - GET_NUM_OBJ_FIELDS_METHOD.setAccessible(true); - - GET_OBJ_FIELD_VALUES_METHOD = ObjectStreamClass.class.getDeclaredMethod( - "getObjFieldValues", new Class[] { Object.class, Object[].class }); - GET_OBJ_FIELD_VALUES_METHOD.setAccessible(true); - - GET_FIELD_METHOD = ObjectStreamField.class.getDeclaredMethod("getField", (Class[])null); - GET_FIELD_METHOD.setAccessible(true); - - HAS_WRITE_REPLACE_METHOD_METHOD = ObjectStreamClass.class.getDeclaredMethod( - "hasWriteReplaceMethod", (Class[])null); - HAS_WRITE_REPLACE_METHOD_METHOD.setAccessible(true); - - INVOKE_WRITE_REPLACE_METHOD = ObjectStreamClass.class.getDeclaredMethod( - "invokeWriteReplace", new Class[] { Object.class }); - INVOKE_WRITE_REPLACE_METHOD.setAccessible(true); - } - catch (Exception e) - { - log.warn("SerializableChecker not available", e); - available = false; - } - } - - private final IObjectChecker[] checkers; - - /** - * Gets whether we can execute the tests. If false, calling {@link #check(Object)} will just - * return and you are advised to rely on the {@link java.io.NotSerializableException}. Clients are - * advised to call this method prior to calling the check method. - * - * @return whether security settings and underlying API etc allow for accessing the - * serialization API using introspection - */ - public static boolean isAvailable() - { - return available; - } - - /** object stack with the trace path. */ - private final LinkedList<TraceSlot> traceStack = new LinkedList<TraceSlot>(); - - /** set for checking circular references. */ - private final Map<Object, Object> checked = new IdentityHashMap<Object, Object>(); - - /** string stack with current names pushed. */ - private final LinkedList<CharSequence> nameStack = new LinkedList<CharSequence>(); - - /** root object being analyzed. */ - private Object root; - - /** set of classes that had no writeObject methods at lookup (to avoid repeated checking) */ - private final Set<Class<?>> writeObjectMethodMissing = new HashSet<Class<?>>(); - - /** current simple field name. */ - private CharSequence simpleName = ""; - - /** current full field description. */ - private String fieldDescription; - - private final Stack<Object> stack = new Stack<Object>(); - - /** - * Constructor. - * - * @param checkers - * the {@link IObjectChecker checkers} that will actually check the objects - * @throws java.io.IOException - * @throws SecurityException - */ - public ObjectChecker(final IObjectChecker... checkers) throws IOException, SecurityException - { - this.checkers = checkers; - } - - private void check(Object obj) - { - if (obj == null) - { - return; - } - - try - { - if (stack.contains(obj)) - { - return; - } - } - catch (RuntimeException e) - { - log.warn("Wasn't possible to check the object '{}' possible due an problematic " + - "implementation of equals method", obj.getClass()); - /* - * Can't check if this obj were in stack, giving up because we don't want to throw an - * invaluable exception to user. The main goal of this checker is to find non - * serializable data - */ - return; - } - - stack.push(obj); - try - { - internalCheck(obj); - } - finally - { - stack.pop(); - } - } - - private void internalCheck(Object obj) - { - if (obj == null) - { - return; - } - - Class<?> cls = obj.getClass(); - nameStack.add(simpleName); - traceStack.add(new TraceSlot(obj, fieldDescription)); - - for (IObjectChecker checker : checkers) - { - IObjectChecker.Result result = checker.check(obj); - if (result.status == IObjectChecker.Result.Status.FAILURE) - { - String prettyPrintMessage = toPrettyPrintedStack(Classes.name(cls)); - String exceptionMessage = result.reason + '\n' + prettyPrintMessage; - throw new ObjectCheckException(exceptionMessage, result.cause); - } - } - - ObjectStreamClass desc; - for (;;) - { - try - { - desc = (ObjectStreamClass)LOOKUP_METHOD.invoke(null, cls, Boolean.TRUE); - Class<?> repCl; - if (!(Boolean)HAS_WRITE_REPLACE_METHOD_METHOD.invoke(desc, (Object[])null) || - (obj = INVOKE_WRITE_REPLACE_METHOD.invoke(desc, obj)) == null || - (repCl = obj.getClass()) == cls) - { - break; - } - cls = repCl; - } - catch (IllegalAccessException e) - { - throw new RuntimeException(e); - } - catch (InvocationTargetException e) - { - throw new RuntimeException(e); - } - } - - if (cls.isPrimitive()) - { - // skip - } - else if (cls.isArray()) - { - checked.put(obj, null); - Class<?> ccl = cls.getComponentType(); - if (!(ccl.isPrimitive())) - { - Object[] objs = (Object[])obj; - for (int i = 0; i < objs.length; i++) - { - CharSequence arrayPos = new StringBuilder(4).append('[').append(i).append(']'); - simpleName = arrayPos; - fieldDescription += arrayPos; - check(objs[i]); - } - } - } - else if (obj instanceof Externalizable && (!Proxy.isProxyClass(cls))) - { - Externalizable extObj = (Externalizable)obj; - try - { - extObj.writeExternal(new ObjectOutputAdaptor() - { - private int count = 0; - - public void writeObject(Object streamObj) throws IOException - { - // Check for circular reference. - if (checked.containsKey(streamObj)) - { - return; - } - - checked.put(streamObj, null); - CharSequence arrayPos = new StringBuilder(10).append("[write:").append(count++).append(']'); - simpleName = arrayPos; - fieldDescription += arrayPos; - - check(streamObj); - } - }); - } - catch (Exception e) - { - if (e instanceof ObjectCheckException) - { - throw (ObjectCheckException)e; - } - log.warn("Error delegating to Externalizable : {}, path: {}", e.getMessage(), currentPath()); - } - } - else - { - Method writeObjectMethod = null; - if (writeObjectMethodMissing.contains(cls) == false) - { - try - { - writeObjectMethod = cls.getDeclaredMethod("writeObject", - new Class[] { java.io.ObjectOutputStream.class }); - } - catch (SecurityException e) - { - // we can't access / set accessible to true - writeObjectMethodMissing.add(cls); - } - catch (NoSuchMethodException e) - { - // cls doesn't have that method - writeObjectMethodMissing.add(cls); - } - } - - final Object original = obj; - if (writeObjectMethod != null) - { - class InterceptingObjectOutputStream extends ObjectOutputStream - { - private int counter; - - InterceptingObjectOutputStream() throws IOException - { - super(DUMMY_OUTPUT_STREAM); - enableReplaceObject(true); - } - - @Override - protected Object replaceObject(Object streamObj) throws IOException - { - if (streamObj == original) - { - return streamObj; - } - - counter++; - // Check for circular reference. - if (checked.containsKey(streamObj)) - { - return null; - } - - checked.put(streamObj, null); - CharSequence arrayPos = new StringBuilder(10).append("[write:").append(counter).append(']'); - simpleName = arrayPos; - fieldDescription += arrayPos; - check(streamObj); - return streamObj; - } - } - try - { - InterceptingObjectOutputStream ioos = new InterceptingObjectOutputStream(); - ioos.writeObject(obj); - } - catch (Exception e) - { - if (e instanceof ObjectCheckException) - { - throw (ObjectCheckException)e; - } - log.warn("error delegating to writeObject : {}, path: {}", e.getMessage(), currentPath()); - } - } - else - { - Object[] slots; - try - { - slots = (Object[])GET_CLASS_DATA_LAYOUT_METHOD.invoke(desc, (Object[])null); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - for (Object slot : slots) - { - ObjectStreamClass slotDesc; - try - { - Field descField = slot.getClass().getDeclaredField("desc"); - descField.setAccessible(true); - slotDesc = (ObjectStreamClass)descField.get(slot); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - checked.put(obj, null); - checkFields(obj, slotDesc); - } - } - } - - traceStack.removeLast(); - nameStack.removeLast(); - } - - private void checkFields(Object obj, ObjectStreamClass desc) - { - int numFields; - try - { - numFields = (Integer)GET_NUM_OBJ_FIELDS_METHOD.invoke(desc, (Object[])null); - } - catch (IllegalAccessException e) - { - throw new RuntimeException(e); - } - catch (InvocationTargetException e) - { - throw new RuntimeException(e); - } - - if (numFields > 0) - { - int numPrimFields; - ObjectStreamField[] fields = desc.getFields(); - Object[] objVals = new Object[numFields]; - numPrimFields = fields.length - objVals.length; - try - { - GET_OBJ_FIELD_VALUES_METHOD.invoke(desc, obj, objVals); - } - catch (IllegalAccessException e) - { - throw new RuntimeException(e); - } - catch (InvocationTargetException e) - { - throw new RuntimeException(e); - } - for (int i = 0; i < objVals.length; i++) - { - if (objVals[i] instanceof String || objVals[i] instanceof Number || - objVals[i] instanceof Date || objVals[i] instanceof Boolean || - objVals[i] instanceof Class) - { - // filter out common cases - continue; - } - - // Check for circular reference. - if (checked.containsKey(objVals[i])) - { - continue; - } - - ObjectStreamField fieldDesc = fields[numPrimFields + i]; - Field field; - try - { - field = (Field)GET_FIELD_METHOD.invoke(fieldDesc, (Object[])null); - } - catch (IllegalAccessException e) - { - throw new RuntimeException(e); - } - catch (InvocationTargetException e) - { - throw new RuntimeException(e); - } - - simpleName = field.getName(); - fieldDescription = field.toString(); - check(objVals[i]); - } - } - } - - /** - * @return name from root to current node concatenated with slashes - */ - private StringBuilder currentPath() - { - StringBuilder b = new StringBuilder(); - for (Iterator<CharSequence> it = nameStack.iterator(); it.hasNext();) - { - b.append(it.next()); - if (it.hasNext()) - { - b.append('/'); - } - } - return b; - } - - /** - * Dump with indentation. - * - * @param type - * the type that couldn't be serialized - * @return A very pretty dump - */ - protected final String toPrettyPrintedStack(String type) - { - StringBuilder result = new StringBuilder(512); - StringBuilder spaces = new StringBuilder(32); - result.append("A problem occurred while checking object with type: "); - result.append(type); - result.append("\nField hierarchy is:"); - for (TraceSlot slot : traceStack) - { - spaces.append(' ').append(' '); - result.append('\n').append(spaces).append(slot.fieldDescription); - result.append(" [class=").append(Classes.name(slot.object.getClass())); - if (slot.object instanceof Component) - { - Component component = (Component)slot.object; - result.append(", path=").append(component.getPath()); - } - result.append(']'); - } - result.append(" <----- field that is causing the problem"); - return result.toString(); - } - - /** - * @see java.io.ObjectOutputStream#writeObjectOverride(java.lang.Object) - */ - @Override - protected final void writeObjectOverride(Object obj) throws IOException - { - if (!available) - { - return; - } - root = obj; - if (fieldDescription == null) - { - fieldDescription = (root instanceof Component) ? ((Component)root).getPath() : ""; - } - - check(root); - } - - /** - * @see java.io.ObjectOutputStream#reset() - */ - @Override - public void reset() throws IOException - { - root = null; - checked.clear(); - fieldDescription = null; - simpleName = null; - traceStack.clear(); - nameStack.clear(); - writeObjectMethodMissing.clear(); - } - - @Override - public void close() throws IOException - { - // do not call super.close() because SerializableChecker uses ObjectOutputStream's no-arg constructor - - // just null-ify the declared members - reset(); - } - -} http://git-wip-us.apache.org/repos/asf/wicket/blob/486b7eb8/wicket-core/src/test/java/org/apache/wicket/util/io/SerializableCheckerTest.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/util/io/SerializableCheckerTest.java b/wicket-core/src/test/java/org/apache/wicket/util/io/SerializableCheckerTest.java index 3331c8d..d82f820 100644 --- a/wicket-core/src/test/java/org/apache/wicket/util/io/SerializableCheckerTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/util/io/SerializableCheckerTest.java @@ -24,7 +24,7 @@ import org.apache.log4j.Level; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.wicket.util.Log4jEventHistory; -import org.apache.wicket.util.objects.checker.ObjectChecker; +import org.apache.wicket.util.objects.checker.CheckingObjectOutputStream; import org.apache.wicket.util.value.ValueMap; import org.junit.Assert; import org.junit.Test; @@ -57,7 +57,7 @@ public class SerializableCheckerTest extends Assert @Test public void runtimeExceptionTolerance() throws IOException { - Logger logger = LogManager.getLogger(ObjectChecker.class); + Logger logger = LogManager.getLogger(CheckingObjectOutputStream.class); logger.setLevel(Level.WARN); Log4jEventHistory logHistory = new Log4jEventHistory(); logger.addAppender(logHistory); @@ -88,7 +88,7 @@ public class SerializableCheckerTest extends Assert { serializableChecker.writeObject(new TestType2()); } - catch (ObjectChecker.ObjectCheckException e) + catch (CheckingObjectOutputStream.ObjectCheckException e) { exceptionMessage = e.getMessage(); }