Friday, April 9, 2010

Fixing the ClassCastException in LayoutComparator.compare()

My client's code was sporadically afflicted with a ClassCastException.  As you can see from the stack trace below, there is no client code involved, just Swing (and swingx) code

java.lang.ClassCastException
at javax.swing.LayoutComparator.compare(LayoutComparator.java:61)
at java.util.Arrays.mergeSort(Arrays.java:1293)
at java.util.Arrays.mergeSort(Arrays.java:1282)
at java.util.Arrays.sort(Arrays.java:1210)
at java.util.Collections.sort(Collections.java:159)
at javax.swing.SortingFocusTraversalPolicy.enumerateAndSortCycle(SortingFocusTraversalPolicy.java:119)
at javax.swing.SortingFocusTraversalPolicy.getFirstComponent(SortingFocusTraversalPolicy.java:434)
at javax.swing.LayoutFocusTraversalPolicy.getFirstComponent(LayoutFocusTraversalPolicy.java:148
at javax.swing.SortingFocusTraversalPolicy.getDefaultComponent(SortingFocusTraversalPolicy.java:511)
at java.awt.FocusTraversalPolicy.getInitialComponent(FocusTraversalPolicy.java:152)
at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:340)
at java.awt.Component.dispatchEventImpl(Component.java:4502)
at java.awt.Container.dispatchEventImpl(Container.java:2099)
at java.awt.Window.dispatchEventImpl(Window.java:2475)
at java.awt.Component.dispatchEvent(Component.java:4460)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:599)
at java.awt.SequencedEvent.dispatch(SequencedEvent.java:101)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:597)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:269)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:184)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:174)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:169)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:161)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)

Google searches revealed that others have seen this error, and supposedly it is caused by Swing components being added or removed while not on the Event Dispatch Thread.  O.K., makes some sense.  But how can you determine which rogue component is causing the problem?  Simple-minded attempts to "hook-into" this stack trace to catch and view the cause are rebuffed by the fact that Component.dispatchEvent()  is final, and all of the dispatchEventImpl()s are package access, not protected.

The place you can access a FocusTraversalPolicy() is in your Container.  However, you cannot access the Comparator it uses, because getComparator() is protected.  So, you have to write your own TraversalPolicy.  The first part is easy.

public class BetterFocusTraversalPolicy extends LayoutFocusTraversalPolicy {

   public BetterFocusTraversalPolicy() {
      setComparator(new BetterLayoutComparator());
   }
}

So, what to use for the "better" Comparator?  I copied and pasted from javax.swing.LayoutComparator, changing a couple of lines.  Here is the original:

public int compare(Object o1, Object o2) {
... some code deleted ...
if (a == null) {
   // 'a' is not part of a Window hierarchy. Can't cope.
   throw new ClassCastException();
}

My first reaction is "why throw a ClassCastException?  (instead of say, an IllegalArgumentException.  But the deeper issue is that the code provides no information about the rogue Component that is not part of the hierarchy.  The proper behavior would be to put o1.toString() into the exception:, e.g.

public int compare(Object o1, Object o2) {
... some code deleted ...
if (a == null) {
   // 'a' is not part of a Window hierarchy. Can't cope.
   throw new ClassCastException(o1.toString());
}

Making this change quickly revealed the problem. One of the axes for a graph was Time, and it was subject to programmatic change in the data collection thread. Correcting this fixed the bug.
But it would sure be simpler if LayoutComparator provided the information in the first place.