001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.wicket.util.io;
018:
019: import java.io.Externalizable;
020: import java.io.IOException;
021: import java.io.NotSerializableException;
022: import java.io.ObjectOutput;
023: import java.io.ObjectOutputStream;
024: import java.io.ObjectStreamClass;
025: import java.io.ObjectStreamField;
026: import java.io.OutputStream;
027: import java.io.Serializable;
028: import java.lang.reflect.Field;
029: import java.lang.reflect.InvocationTargetException;
030: import java.lang.reflect.Method;
031: import java.lang.reflect.Proxy;
032: import java.util.Date;
033: import java.util.HashMap;
034: import java.util.IdentityHashMap;
035: import java.util.Iterator;
036: import java.util.LinkedList;
037: import java.util.Map;
038:
039: import org.apache.wicket.Component;
040: import org.apache.wicket.WicketRuntimeException;
041: import org.slf4j.Logger;
042: import org.slf4j.LoggerFactory;
043:
044: /**
045: * Utility class that analyzes objects for non-serializable nodes. Construct
046: * with the object you want to check, and then call {@link #check()}. When a
047: * non-serializable object is found, a {@link WicketNotSerializableException} is
048: * thrown with a message that shows the trace up to the not-serializable object.
049: * The exception is thrown for the first non-serializable instance it
050: * encounters, so multiple problems will not be shown.
051: * <p>
052: * As this class depends heavily on JDK's serialization internals using
053: * introspection, analyzing may not be possible, for instance when the runtime
054: * environment does not have sufficient rights to set fields accesible that
055: * would otherwise be hidden. You should call
056: * {@link SerializableChecker#isAvailable()} to see whether this class can
057: * operate properly. If it doesn't, you should fall back to e.g. re-throwing/
058: * printing the {@link NotSerializableException} you probably got before using
059: * this class.
060: * </p>
061: *
062: * @author eelcohillenius
063: * @author Al Maw
064: */
065: public final class SerializableChecker extends ObjectOutputStream {
066: /**
067: * Exception that is thrown when a non-serializable object was found.
068: */
069: public static final class WicketNotSerializableException extends
070: WicketRuntimeException {
071: private static final long serialVersionUID = 1L;
072:
073: WicketNotSerializableException(String message, Throwable cause) {
074: super (message, cause);
075: }
076: }
077:
078: /**
079: * Does absolutely nothing.
080: */
081: private static class NoopOutputStream extends OutputStream {
082: public void close() {
083: }
084:
085: public void flush() {
086: }
087:
088: public void write(byte[] b) {
089: }
090:
091: public void write(byte[] b, int i, int l) {
092: }
093:
094: public void write(int b) {
095: }
096: }
097:
098: private static abstract class ObjectOutputAdaptor implements
099: ObjectOutput {
100:
101: public void close() throws IOException {
102: }
103:
104: public void flush() throws IOException {
105: }
106:
107: public void write(byte[] b) throws IOException {
108: }
109:
110: public void write(byte[] b, int off, int len)
111: throws IOException {
112: }
113:
114: public void write(int b) throws IOException {
115: }
116:
117: public void writeBoolean(boolean v) throws IOException {
118: }
119:
120: public void writeByte(int v) throws IOException {
121: }
122:
123: public void writeBytes(String s) throws IOException {
124: }
125:
126: public void writeChar(int v) throws IOException {
127: }
128:
129: public void writeChars(String s) throws IOException {
130: }
131:
132: public void writeDouble(double v) throws IOException {
133: }
134:
135: public void writeFloat(float v) throws IOException {
136: }
137:
138: public void writeInt(int v) throws IOException {
139: }
140:
141: public void writeLong(long v) throws IOException {
142: }
143:
144: public void writeShort(int v) throws IOException {
145: }
146:
147: public void writeUTF(String str) throws IOException {
148: }
149: }
150:
151: /** Holds information about the field and the resulting object being traced. */
152: private static final class TraceSlot {
153: private final String fieldDescription;
154:
155: private final Object object;
156:
157: TraceSlot(Object object, String fieldDescription) {
158: super ();
159: this .object = object;
160: this .fieldDescription = fieldDescription;
161: }
162:
163: public String toString() {
164: return object.getClass() + " - " + fieldDescription;
165: }
166: }
167:
168: private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new NoopOutputStream();
169:
170: /** log. */
171: private static final Logger log = LoggerFactory
172: .getLogger(SerializableChecker.class);
173:
174: /** Whether we can execute the tests. If false, check will just return. */
175: private static boolean available = true;
176:
177: // this hack - accessing the serialization API through introspection - is
178: // the only way to use Java serialization for our purposes without writing
179: // the whole thing from scratch (and even then, it would be limited). This
180: // way of working is of course fragile for internal API changes, but as we
181: // do an extra check on availability and we report when we can't use this
182: // introspection fu, we'll find out soon enough and clients on this class
183: // can fall back on Java's default exception for serialization errors (which
184: // sucks and is the main reason for this attempt).
185: private static final Method LOOKUP_METHOD;
186:
187: private static final Method GET_CLASS_DATA_LAYOUT_METHOD;
188:
189: private static final Method GET_NUM_OBJ_FIELDS_METHOD;
190:
191: private static final Method GET_OBJ_FIELD_VALUES_METHOD;
192:
193: private static final Method GET_FIELD_METHOD;
194:
195: private static final Method HAS_WRITE_REPLACE_METHOD_METHOD;
196:
197: private static final Method INVOKE_WRITE_REPLACE_METHOD;
198:
199: static {
200: try {
201: LOOKUP_METHOD = ObjectStreamClass.class
202: .getDeclaredMethod("lookup", new Class[] {
203: Class.class, Boolean.TYPE });
204: LOOKUP_METHOD.setAccessible(true);
205:
206: GET_CLASS_DATA_LAYOUT_METHOD = ObjectStreamClass.class
207: .getDeclaredMethod("getClassDataLayout", null);
208: GET_CLASS_DATA_LAYOUT_METHOD.setAccessible(true);
209:
210: GET_NUM_OBJ_FIELDS_METHOD = ObjectStreamClass.class
211: .getDeclaredMethod("getNumObjFields", null);
212: GET_NUM_OBJ_FIELDS_METHOD.setAccessible(true);
213:
214: GET_OBJ_FIELD_VALUES_METHOD = ObjectStreamClass.class
215: .getDeclaredMethod(
216: "getObjFieldValues",
217: new Class[] { Object.class, Object[].class });
218: GET_OBJ_FIELD_VALUES_METHOD.setAccessible(true);
219:
220: GET_FIELD_METHOD = ObjectStreamField.class
221: .getDeclaredMethod("getField", null);
222: GET_FIELD_METHOD.setAccessible(true);
223:
224: HAS_WRITE_REPLACE_METHOD_METHOD = ObjectStreamClass.class
225: .getDeclaredMethod("hasWriteReplaceMethod", null);
226: HAS_WRITE_REPLACE_METHOD_METHOD.setAccessible(true);
227:
228: INVOKE_WRITE_REPLACE_METHOD = ObjectStreamClass.class
229: .getDeclaredMethod("invokeWriteReplace",
230: new Class[] { Object.class });
231: INVOKE_WRITE_REPLACE_METHOD.setAccessible(true);
232: } catch (SecurityException e) {
233: available = false;
234: throw new RuntimeException(e);
235: } catch (NoSuchMethodException e) {
236: available = false;
237: throw new RuntimeException(e);
238: }
239: }
240:
241: /**
242: * Gets whether we can execute the tests. If false, calling {@link #check()}
243: * will just return and you are advised to rely on the
244: * {@link NotSerializableException}. Clients are advised to call this
245: * method prior to calling the check method.
246: *
247: * @return whether security settings and underlying API etc allow for
248: * accessing the serialization API using introspection
249: */
250: public static boolean isAvailable() {
251: return available;
252: }
253:
254: /** object stack that with the trace path. */
255: private final LinkedList traceStack = new LinkedList();
256:
257: /** set for checking circular references. */
258: private final Map checked = new IdentityHashMap();
259:
260: /** string stack with current names pushed. */
261: private final LinkedList nameStack = new LinkedList();
262:
263: /** root object being analyzed. */
264: private Object root;
265:
266: /** cache for classes - writeObject methods. */
267: private final Map writeObjectMethodCache = new HashMap();
268:
269: /** current simple field name. */
270: private String simpleName = "";
271:
272: /** current full field description. */
273: private String fieldDescription;
274:
275: /** Exception that should be set as the cause when throwing a new exception. */
276: private final NotSerializableException exception;
277:
278: /**
279: * Construct.
280: *
281: * @param exception
282: * exception that should be set as the cause when throwing a new
283: * exception
284: *
285: * @throws IOException
286: */
287: public SerializableChecker(NotSerializableException exception)
288: throws IOException {
289: this .exception = exception;
290: }
291:
292: /**
293: * @see java.io.ObjectOutputStream#reset()
294: */
295: public void reset() throws IOException {
296: root = null;
297: checked.clear();
298: fieldDescription = null;
299: simpleName = null;
300: traceStack.clear();
301: nameStack.clear();
302: writeObjectMethodCache.clear();
303: }
304:
305: private void check(Object obj) {
306: if (obj == null) {
307: return;
308: }
309:
310: Class cls = obj.getClass();
311: nameStack.add(simpleName);
312: traceStack.add(new TraceSlot(obj, fieldDescription));
313:
314: if (!(obj instanceof Serializable)
315: && (!Proxy.isProxyClass(cls))) {
316: throw new WicketNotSerializableException(
317: toPrettyPrintedStack(obj.getClass().getName())
318: .toString(), exception);
319: }
320:
321: ObjectStreamClass desc;
322: for (;;) {
323: try {
324: desc = (ObjectStreamClass) LOOKUP_METHOD.invoke(null,
325: new Object[] { cls, Boolean.TRUE });
326: Class repCl;
327: if (!((Boolean) HAS_WRITE_REPLACE_METHOD_METHOD.invoke(
328: desc, null)).booleanValue()
329: || (obj = INVOKE_WRITE_REPLACE_METHOD.invoke(
330: desc, new Object[] { obj })) == null
331: || (repCl = obj.getClass()) == cls) {
332: break;
333: }
334: cls = repCl;
335: } catch (IllegalAccessException e) {
336: throw new RuntimeException(e);
337: } catch (InvocationTargetException e) {
338: throw new RuntimeException(e);
339: }
340: }
341:
342: if (cls.isPrimitive()) {
343: // skip
344: } else if (cls.isArray()) {
345: checked.put(obj, null);
346: Class ccl = cls.getComponentType();
347: if (!(ccl.isPrimitive())) {
348: Object[] objs = (Object[]) obj;
349: for (int i = 0; i < objs.length; i++) {
350: String arrayPos = "[" + i + "]";
351: simpleName = arrayPos;
352: fieldDescription += arrayPos;
353: check(objs[i]);
354: }
355: }
356: } else if (obj instanceof Externalizable
357: && (!Proxy.isProxyClass(cls))) {
358: Externalizable extObj = (Externalizable) obj;
359: try {
360: extObj.writeExternal(new ObjectOutputAdaptor() {
361: private int count = 0;
362:
363: public void writeObject(Object streamObj)
364: throws IOException {
365: // Check for circular reference.
366: if (checked.containsKey(streamObj)) {
367: return;
368: }
369:
370: checked.put(streamObj, null);
371: String arrayPos = "[write:" + count++ + "]";
372: simpleName = arrayPos;
373: fieldDescription += arrayPos;
374:
375: check(streamObj);
376: }
377: });
378: } catch (Exception e) {
379: if (e instanceof WicketNotSerializableException) {
380: throw (WicketNotSerializableException) e;
381: }
382: log.warn("error delegating to Externalizable : "
383: + e.getMessage() + ", path: " + currentPath());
384: }
385: } else {
386: Method writeObjectMethod = null;
387: Object o = writeObjectMethodCache.get(cls);
388: if (o != null) {
389: if (o instanceof Method) {
390: writeObjectMethod = (Method) o;
391: }
392: } else {
393: try {
394: writeObjectMethod = cls
395: .getDeclaredMethod(
396: "writeObject",
397: new Class[] { java.io.ObjectOutputStream.class });
398: } catch (SecurityException e) {
399: // we can't access/ set accessible to true
400: writeObjectMethodCache.put(cls, Boolean.FALSE);
401: } catch (NoSuchMethodException e) {
402: // cls doesn't have that method
403: writeObjectMethodCache.put(cls, Boolean.FALSE);
404: }
405: }
406:
407: final Object original = obj;
408: if (writeObjectMethod != null) {
409: class InterceptingObjectOutputStream extends
410: ObjectOutputStream {
411: private int counter;
412:
413: InterceptingObjectOutputStream() throws IOException {
414: super (DUMMY_OUTPUT_STREAM);
415: enableReplaceObject(true);
416: }
417:
418: protected Object replaceObject(Object streamObj)
419: throws IOException {
420: if (streamObj == original) {
421: return streamObj;
422: }
423:
424: counter++;
425: // Check for circular reference.
426: if (checked.containsKey(streamObj)) {
427: return null;
428: }
429:
430: checked.put(original, null);
431: String arrayPos = "[write:" + counter + "]";
432: simpleName = arrayPos;
433: fieldDescription += arrayPos;
434: check(streamObj);
435: return streamObj;
436: }
437: }
438: try {
439: InterceptingObjectOutputStream ioos = new InterceptingObjectOutputStream();
440: ioos.writeObject(obj);
441: } catch (Exception e) {
442: if (e instanceof WicketNotSerializableException) {
443: throw (WicketNotSerializableException) e;
444: }
445: log.warn("error delegating to writeObject : "
446: + e.getMessage() + ", path: "
447: + currentPath());
448: }
449: } else {
450: Object[] slots;
451: try {
452: slots = (Object[]) GET_CLASS_DATA_LAYOUT_METHOD
453: .invoke(desc, null);
454: } catch (Exception e) {
455: throw new RuntimeException(e);
456: }
457: for (int i = 0; i < slots.length; i++) {
458: ObjectStreamClass slotDesc;
459: try {
460: Field descField = slots[i].getClass()
461: .getDeclaredField("desc");
462: descField.setAccessible(true);
463: slotDesc = (ObjectStreamClass) descField
464: .get(slots[i]);
465: } catch (Exception e) {
466: throw new RuntimeException(e);
467: }
468: checked.put(obj, null);
469: checkFields(obj, slotDesc);
470: }
471: }
472: }
473:
474: traceStack.removeLast();
475: nameStack.removeLast();
476: }
477:
478: private void checkFields(Object obj, ObjectStreamClass desc) {
479: int numFields;
480: try {
481: numFields = ((Integer) GET_NUM_OBJ_FIELDS_METHOD.invoke(
482: desc, null)).intValue();
483: } catch (IllegalAccessException e) {
484: throw new RuntimeException(e);
485: } catch (InvocationTargetException e) {
486: throw new RuntimeException(e);
487: }
488:
489: if (numFields > 0) {
490: int numPrimFields;
491: ObjectStreamField[] fields = desc.getFields();
492: Object[] objVals = new Object[numFields];
493: numPrimFields = fields.length - objVals.length;
494: try {
495: GET_OBJ_FIELD_VALUES_METHOD.invoke(desc, new Object[] {
496: obj, objVals });
497: } catch (IllegalAccessException e) {
498: throw new RuntimeException(e);
499: } catch (InvocationTargetException e) {
500: throw new RuntimeException(e);
501: }
502: for (int i = 0; i < objVals.length; i++) {
503: if (objVals[i] instanceof String
504: || objVals[i] instanceof Number
505: || objVals[i] instanceof Date
506: || objVals[i] instanceof Boolean
507: || objVals[i] instanceof Class) {
508: // fitler out common cases
509: continue;
510: }
511:
512: // Check for circular reference.
513: if (checked.containsKey(objVals[i])) {
514: continue;
515: }
516:
517: ObjectStreamField fieldDesc = fields[numPrimFields + i];
518: Field field;
519: try {
520: field = (Field) GET_FIELD_METHOD.invoke(fieldDesc,
521: null);
522: } catch (IllegalAccessException e) {
523: throw new RuntimeException(e);
524: } catch (InvocationTargetException e) {
525: throw new RuntimeException(e);
526: }
527:
528: String fieldName = field.getName();
529: simpleName = field.getName();
530: fieldDescription = field.toString();
531: check(objVals[i]);
532: }
533: }
534: }
535:
536: /**
537: * @return name from root to current node concatted with slashes
538: */
539: private StringBuffer currentPath() {
540: StringBuffer b = new StringBuffer();
541: for (Iterator it = nameStack.iterator(); it.hasNext();) {
542: b.append(it.next());
543: if (it.hasNext()) {
544: b.append('/');
545: }
546: }
547: return b;
548: }
549:
550: /**
551: * Dump with identation.
552: *
553: * @param type
554: * the type that couldn't be serialized
555: * @return A very pretty dump
556: */
557: private final String toPrettyPrintedStack(String type) {
558: StringBuffer result = new StringBuffer();
559: StringBuffer spaces = new StringBuffer();
560: result.append("Unable to serialize class: ");
561: result.append(type);
562: result.append("\nField hierarchy is:");
563: for (Iterator i = traceStack.listIterator(); i.hasNext();) {
564: spaces.append(" ");
565: TraceSlot slot = (TraceSlot) i.next();
566: result.append("\n").append(spaces).append(
567: slot.fieldDescription);
568: result.append(" [class=").append(
569: slot.object.getClass().getName());
570: if (slot.object instanceof Component) {
571: Component component = (Component) slot.object;
572: result.append(", path=").append(component.getPath());
573: }
574: result.append("]");
575: }
576: result.append(" <----- field that is not serializable");
577: return result.toString();
578: }
579:
580: /**
581: * @see java.io.ObjectOutputStream#writeObjectOverride(java.lang.Object)
582: */
583: protected final void writeObjectOverride(Object obj)
584: throws IOException {
585: if (!available) {
586: return;
587: }
588: root = obj;
589: if (fieldDescription == null) {
590: fieldDescription = (root instanceof Component) ? ((Component) root)
591: .getPath()
592: : "";
593: }
594:
595: check(root);
596: }
597: }
|