Why do you throw WicketNotSerializableException when the model is still
detached?
On Fri, Sep 5, 2008 at 11:11 AM, Kaspar Fischer <[EMAIL PROTECTED]>wrote:
> For the sake of completeness, here is the solution I am currently using. It
> uses,
> as suggested by Martijn, a custom request cycle and a modified version of
> SerializableChecker. You have to install the custom request cycle in your
> application
> using
>
> @Override
> public RequestCycle newRequestCycle(Request request, Response response)
> {
> return new CustomRequestCycle(this, (WebRequest) request, (WebResponse)
> response);
> }
>
> Hope this helps others, too!
> Kaspar
>
> // ***** FILE: CustomRequestCycle.java *****
> import java.io.NotSerializableException;
>
> import org.apache.wicket.Page;
> import org.apache.wicket.Response;
> import org.apache.wicket.protocol.http.WebApplication;
> import org.apache.wicket.protocol.http.WebRequest;
> import org.apache.wicket.protocol.http.WebRequestCycle;
> import org.apache.wicket.request.target.component.IPageRequestTarget;
> import org.slf4j.Logger;
> import org.slf4j.LoggerFactory;
>
> /**
> * A custom request cycle that checks, when in development mode, that all
> models of a page are
> * detached. Currently, only model that are instances of
> LoadableDetachableModel (including
> * subclasses) are checked.
> */
> public class CustomRequestCycle extends WebRequestCycle
> {
> /** Logging object */
> private static final Logger log =
> LoggerFactory.getLogger(WebRequestCycle.class);
>
> public CustomRequestCycle(WebApplication application, WebRequest request,
> Response response)
> {
> super(application, request, response);
> }
>
> @Override
> protected void onEndRequest()
> {
> super.onEndRequest();
>
> if
> (WebApplication.DEVELOPMENT.equalsIgnoreCase(WebApplication.get().getConfigurationType()))
> {
> Page requestPage = getRequest().getPage();
> testDetachedObjects(requestPage);
>
> if (getRequestTarget() instanceof IPageRequestTarget)
> {
> Page responsePage = ((IPageRequestTarget)
> getRequestTarget()).getPage();
>
> if (responsePage != requestPage)
> {
> testDetachedObjects(responsePage);
> }
> }
> }
> }
>
> private void testDetachedObjects(final Page page)
> {
> if (page == null)
> {
> return;
> }
>
> try
> {
> NotSerializableException exception = new NotSerializableException(
> "Model is not detached when attempting to serialize!");
> DetachedChecker checker = new DetachedChecker(exception);
> checker.writeObject(page);
> }
> catch (Exception ex)
> {
> log.error("Couldn't test/serialize the Page: " + page + ", error: " +
> ex);
> }
> }
> }
>
>
> // ***** FILE: DetachedChecker.java *****
> import java.io.Externalizable;
> import java.io.IOException;
> import java.io.NotSerializableException;
> import java.io.ObjectOutput;
> import java.io.ObjectOutputStream;
> import java.io.ObjectStreamClass;
> import java.io.ObjectStreamField;
> import java.io.OutputStream;
> import java.io.Serializable;
> 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.IdentityHashMap;
> import java.util.Iterator;
> import java.util.LinkedList;
> import java.util.Map;
>
> import org.apache.wicket.Component;
> import org.apache.wicket.WicketRuntimeException;
> import org.apache.wicket.model.LoadableDetachableModel;
> import org.apache.wicket.util.lang.Generics;
> import org.slf4j.Logger;
> import org.slf4j.LoggerFactory;
>
> /**
> * This is taken from Wicket SerializableChecker.java (SVN r687197) and
> customized slightly
> * (see comments containing "KF"). See the latter file for all details,
> including terms of
> * use. Notice that this does not replace SerializableChecker; the latter
> is still run.
> */
> public final class DetachedChecker extends ObjectOutputStream
> {
> /**
> * Exception that is thrown when a non-serializable object was found.
> */
> public static final class WicketNotSerializableException extends
> WicketRuntimeException
> {
> private static final long serialVersionUID = 1L;
>
> WicketNotSerializableException(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)
> {
> super();
> this.object = object;
> this.fieldDescription = fieldDescription;
> }
>
> @Override
> public String toString()
> {
> return object.getClass() + " - " + fieldDescription;
> }
> }
>
> private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new
> NoopOutputStream();
>
> /** log. */
> private static final Logger log =
> LoggerFactory.getLogger(DetachedChecker.class);
>
> /** 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 final Method LOOKUP_METHOD;
>
> private static final Method GET_CLASS_DATA_LAYOUT_METHOD;
>
> private static final Method GET_NUM_OBJ_FIELDS_METHOD;
>
> private static final Method GET_OBJ_FIELD_VALUES_METHOD;
>
> private static final Method GET_FIELD_METHOD;
>
> private static final Method HAS_WRITE_REPLACE_METHOD_METHOD;
>
> private static final 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 (SecurityException e)
> {
> available = false;
> throw new RuntimeException(e);
> }
> catch (NoSuchMethodException e)
> {
> available = false;
> throw new RuntimeException(e);
> }
> }
>
> /**
> * Gets whether we can execute the tests. If false, calling [EMAIL PROTECTED]
> #check(Object)} will just
> * return and you are advised to rely on the [EMAIL PROTECTED]
> 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 that 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<String> nameStack = new LinkedList<String>();
>
> /** root object being analyzed. */
> private Object root;
>
> /** cache for classes - writeObject methods. */
> private final Map<Class<?>, Object> writeObjectMethodCache =
> Generics.newHashMap();
>
> /** current simple field name. */
> private String simpleName = "";
>
> /** current full field description. */
> private String fieldDescription;
>
> /** Exception that should be set as the cause when throwing a new
> exception. */
> private final NotSerializableException exception;
>
> /**
> * Construct.
> *
> * @param exception
> * exception that should be set as the cause when throwing a new
> exception
> *
> * @throws IOException
> */
> public DetachedChecker(NotSerializableException exception) throws
> IOException
> {
> this.exception = exception;
> }
>
> /**
> * @see java.io.ObjectOutputStream#reset()
> */
> @Override
> public void reset() throws IOException
> {
> root = null;
> checked.clear();
> fieldDescription = null;
> simpleName = null;
> traceStack.clear();
> nameStack.clear();
> writeObjectMethodCache.clear();
> }
>
> private void check(Object obj)
> {
> if (obj == null)
> {
> return;
> }
>
> Class<?> cls = obj.getClass();
> nameStack.add(simpleName);
> traceStack.add(new TraceSlot(obj, fieldDescription));
>
> if (!(obj instanceof Serializable) && (!Proxy.isProxyClass(cls)))
> {
> throw new
> WicketNotSerializableException(toPrettyPrintedStack(obj.getClass().getName()),
> exception);
> }
>
> // BEGIN KF
> if (obj instanceof LoadableDetachableModel)
> {
> LoadableDetachableModel model = (LoadableDetachableModel) obj;
> if (model.isAttached())
> {
> Object value = model.getObject();
> throw new
> WicketNotSerializableException(toPrettyPrintedStack(obj.getClass().getName())
> + "\nmodel object: "
> + value, exception);
> }
> }
> // END KF
>
> ObjectStreamClass desc;
> for (;;)
> {
> try
> {
> desc = (ObjectStreamClass) LOOKUP_METHOD.invoke(null, new Object[] {
> cls, Boolean.TRUE
> });
> Class<?> repCl;
> if (!((Boolean) HAS_WRITE_REPLACE_METHOD_METHOD.invoke(desc,
> (Object[]) null)).booleanValue()
> || (obj = INVOKE_WRITE_REPLACE_METHOD.invoke(desc, new Object[]
> {
> 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++)
> {
> String arrayPos = "[" + i + "]";
> 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);
> String arrayPos = "[write:" + count++ + "]";
> simpleName = arrayPos;
> fieldDescription += arrayPos;
>
> check(streamObj);
> }
> });
> }
> catch (Exception e)
> {
> if (e instanceof WicketNotSerializableException)
> {
> throw (WicketNotSerializableException) e;
> }
> log.warn("error delegating to Externalizable : " + e.getMessage() +
> ", path: " + currentPath());
> }
> }
> else
> {
> Method writeObjectMethod = null;
> Object o = writeObjectMethodCache.get(cls);
> if (o != null)
> {
> if (o instanceof Method)
> {
> writeObjectMethod = (Method) o;
> }
> }
> else
> {
> try
> {
> writeObjectMethod = cls.getDeclaredMethod("writeObject", new
> Class[] {
> java.io.ObjectOutputStream.class
> });
> }
> catch (SecurityException e)
> {
> // we can't access/ set accessible to true
> writeObjectMethodCache.put(cls, Boolean.FALSE);
> }
> catch (NoSuchMethodException e)
> {
> // cls doesn't have that method
> writeObjectMethodCache.put(cls, Boolean.FALSE);
> }
> }
>
> 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(original, null);
> String arrayPos = "[write:" + counter + "]";
> simpleName = arrayPos;
> fieldDescription += arrayPos;
> check(streamObj);
> return streamObj;
> }
> }
> try
> {
> InterceptingObjectOutputStream ioos = new
> InterceptingObjectOutputStream();
> ioos.writeObject(obj);
> }
> catch (Exception e)
> {
> if (e instanceof WicketNotSerializableException)
> {
> throw (WicketNotSerializableException) e;
> }
> log.warn("error delegating to writeObject : " + e.getMessage() +
> ", path: " + currentPath());
> }
> }
> else
> {
> Object[] slots;
> try
> {
> slots = (Object[]) GET_CLASS_DATA_LAYOUT_METHOD.invoke(desc,
> (Object[]) null);
> }
> catch (Exception e)
> {
> throw new RuntimeException(e);
> }
> for (int i = 0; i < slots.length; i++)
> {
> ObjectStreamClass slotDesc;
> try
> {
> Field descField = slots[i].getClass().getDeclaredField("desc");
> descField.setAccessible(true);
> slotDesc = (ObjectStreamClass) descField.get(slots[i]);
> }
> 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)).intValue();
> }
> 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, new Object[] {
> 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);
> }
>
> String fieldName = field.getName();
> simpleName = field.getName();
> fieldDescription = field.toString();
> check(objVals[i]);
> }
> }
> }
>
> /**
> * @return name from root to current node concatenated with slashes
> */
> private StringBuffer currentPath()
> {
> StringBuffer b = new StringBuffer();
> for (Iterator<String> 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
> */
> private final String toPrettyPrintedStack(String type)
> {
> StringBuffer result = new StringBuffer();
> StringBuffer spaces = new StringBuffer();
> result.append("Unable to serialize class: ");
> result.append(type);
> result.append("\nField hierarchy is:");
> for (Iterator<TraceSlot> i = traceStack.listIterator(); i.hasNext();)
> {
> spaces.append(" ");
> TraceSlot slot = i.next();
> result.append("\n").append(spaces).append(slot.fieldDescription);
> result.append(" [class=").append(slot.object.getClass().getName());
> if (slot.object instanceof Component)
> {
> Component component = (Component) slot.object;
> result.append(", path=").append(component.getPath());
> }
> result.append("]");
> result.append(" {object:" + slot.object + "}");
> }
> result.append(" <----- model that is not detached"); // KF
> 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);
> }
> }
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: [EMAIL PROTECTED]
> For additional commands, e-mail: [EMAIL PROTECTED]
>
>
--
Eyal Golan
[EMAIL PROTECTED]
Visit: http://jvdrums.sourceforge.net/
LinkedIn: http://www.linkedin.com/in/egolan74
P Save a tree. Please don't print this e-mail unless it's really necessary