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]