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