001    /*
002     * Copyright 2002-2004 the original author or authors.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.util;
017    
018    import java.awt.AWTEvent;
019    import java.awt.BorderLayout;
020    import java.awt.Component;
021    import java.awt.Dimension;
022    import java.awt.EventQueue;
023    import java.awt.LayoutManager;
024    import java.awt.Rectangle;
025    import java.awt.Toolkit;
026    import java.lang.ref.ReferenceQueue;
027    import java.lang.ref.PhantomReference;
028    import java.lang.reflect.InvocationTargetException;
029    import java.util.concurrent.FutureTask;
030    import java.util.concurrent.Callable;
031    
032    import javax.swing.JComponent;
033    import javax.swing.JLabel;
034    import javax.swing.JPanel;
035    import javax.swing.JScrollPane;
036    import javax.swing.JTextField;
037    import javax.swing.Scrollable;
038    import javax.swing.JRootPane;
039    import javax.swing.JLayeredPane;
040    import javax.swing.SwingUtilities;
041    
042    import org.springframework.richclient.test.SpringRichTestCase;
043    
044    /**
045     * @author andy
046     * @since May 12, 2006 8:32:27 AM
047     */
048    public class OverlayHelperTests extends SpringRichTestCase {
049    
050        /**
051         * OverlayHelper installs the overlay as the View of a JScrollPane viewport, if the component is in a JScrollPane,
052         * so that the overlay is shown in the proper location when scrolled. However, to accomplish this, it will remove
053         * the component that was in the viewport, add it to a JLayeredPane, and then add that JLayeredPane to the viewport
054         * instead. This introduced a bug if the viewport's view happened to implement the Scrollable interface, since
055         * JScrollPane does <i>not</i> implement the Scrollable interface. See issue RCP-344.
056         * 
057         * @throws Exception
058         */
059        public void testRegressionScrollableProxy() throws Exception {
060            performScrollableTest();
061            performNonScrollableTest();
062        }
063    
064    
065        /**
066         * See <a href="http://opensource.atlassian.com/projects/spring/browse/RCP-492">RCP-492<a/>
067         * for details on this test and related bug.
068         *
069         * @throws Exception
070         */
071        public void testRegressionOverlayHelperLeak() throws Exception {
072            // Basically, this test will simulate adding a component to a
073            // JRootPane, then attach an overlay to it.  The overlay will be
074            // installed into the layered pane of the JRootPane at the
075            // PALETTE_LAYER.  This test will then remove the component from the
076            // JRootPane, ensure the overlay is removed from the layered pane,
077            // and then further ensure that nothing else holds a strong reference
078            // to the component (by waiting for the component to be garbage
079            // collected).
080            JComponent component = createTestComponent();
081            JComponent overlay = createTestOverlay();
082            final ReferenceQueue rq = new ReferenceQueue();
083            final PhantomReference componentRef = new PhantomReference(component, rq);
084    
085            final JRootPane rootPane = new JRootPane() {
086                public boolean isVisible() {
087                    return true;
088                }
089                public boolean isShowing() {
090                    return true;
091                }
092                protected JLayeredPane createLayeredPane() {
093                    return new JLayeredPane() {
094                        public boolean isVisible() {
095                            return true;
096                        }
097                        public boolean isShowing() {
098                            return true;
099                        }
100                    };
101                }
102            };
103            
104            final int lpcount = rootPane.getLayeredPane().getComponentCountInLayer(JLayeredPane.PALETTE_LAYER.intValue());
105            
106            OverlayHelper.attachOverlay(overlay, component, 0, 0, 0);
107            
108            assertEquals(lpcount, rootPane.getLayeredPane().getComponentCountInLayer(JLayeredPane.PALETTE_LAYER.intValue()));
109            rootPane.getContentPane().add(component);
110            // updateOverlay is "invokedLater", so wait for it...
111            waitUntilEventQueueIsEmpty();
112            assertEquals(lpcount + 1, rootPane.getLayeredPane().getComponentCountInLayer(JLayeredPane.PALETTE_LAYER.intValue()));
113            
114            rootPane.getContentPane().remove(component);
115            
116            // the remove operation make be invoked later, so wait for it...
117            waitUntilEventQueueIsEmpty();
118            
119            // Clear out our strong references so it gets garbage collected.
120            component = null;
121            overlay = null;
122            
123            // Make sure the overlay was removed from the layered pane.
124            assertEquals("It appears the overlay was not removed from the layered pane when its component was removed from the content pane", lpcount, rootPane.getLayeredPane().getComponentCountInLayer(JLayeredPane.PALETTE_LAYER.intValue()));
125            
126            // Make sure no other references are held... wait up to 15 seconds for
127            // the object to be garbage collected.
128            // Note: we may want to remove this section from the test as it
129            // presumes the VM will garbage collect the reference fairly quickly
130            // once a System.gc() is invoked.
131            // There is no guarantee this will happen, so the assumption may be
132            // faulty.  If it ends up being a problem, or starts yielding
133            // inconsistent test results, then feel free to yank it.
134            PhantomReference pr;
135            final long end = System.currentTimeMillis() + 15000;
136            do {
137                System.gc();
138            } while((pr = (PhantomReference)rq.remove(100)) == null && System.currentTimeMillis() < end);
139            if(pr != null) {
140                pr.clear();
141            }
142            assertSame("Either something else is still holding a strong reference to the component, or the VM is not garbage collecting it.  See comments in OverlayHelperTests.testRegressionOverlayHelperLeak() for more detail", pr, componentRef);
143        }
144    
145        /**
146         * Ensures that OverlayHelper supports the Scrollable interface and properly proxies Scrollable methods.
147         * 
148         * @throws Exception
149         */
150        private void performScrollableTest() throws Exception {
151            final ScrollablePanel view = new ScrollablePanel(new BorderLayout());
152            view.setScrollableUnitIncrement(5);
153            view.setScrollableBlockIncrement(30);
154            view.setScrollableTracksViewportWidth(true);
155    
156            final JComponent overlay = createTestOverlay();
157            final JComponent someField = createTestComponent();
158    
159            OverlayHelper.attachOverlay(overlay, someField, 0, 0, 0);
160    
161            view.add(someField);
162    
163            final JScrollPane scrollPane = new JScrollPane(view);
164    
165            waitUntilEventQueueIsEmpty();
166    
167            final Component viewportView = scrollPane.getViewport().getView();
168    
169            // If OverlayHelper changes the way it handles scrollable overlays,
170            // then the test will need to be revisited - this makes sure it
171            // won't get ignored. :)
172    //        assertFalse(viewportView == view);
173    
174            assertTrue(viewportView instanceof Scrollable);
175            assertTrue(((Scrollable) viewportView).getScrollableTracksViewportWidth());
176            assertFalse(((Scrollable) viewportView).getScrollableTracksViewportHeight());
177            assertEquals(5, ((Scrollable) viewportView).getScrollableUnitIncrement(null, 0, 0));
178            assertEquals(30, ((Scrollable) viewportView).getScrollableBlockIncrement(null, 0, 0));
179            assertEquals(view.getPreferredScrollableViewportSize(), ((Scrollable) viewportView)
180                    .getPreferredScrollableViewportSize());
181        }
182    
183        private void waitUntilEventQueueIsEmpty() throws InterruptedException, InvocationTargetException {
184            // we have to sleep here until the asynchronously attachement of JLayeredPane and the overlay is finished
185            EventQueue eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
186            AWTEvent peekEvent;
187            while((peekEvent = eventQueue.peekEvent()) != null) {
188                System.out.println("got event in queue: " + peekEvent);
189                Thread.currentThread().sleep(0);
190            }
191            // Added 10/10/07 AlD: Sometimes the above loop fails to work!?  This
192            // should further ensure the event loop is empty.
193            SwingUtilities.invokeAndWait(new Runnable() {
194                public void run() { }
195            });
196        }
197    
198        /**
199         * Ensures that OverlayHelper will NOT implement the Scrollable interface if the view component does not implement
200         * the Scrollable interface.
201         * 
202         * @throws Exception
203         */
204        private void performNonScrollableTest() throws Exception {
205            final JPanel view = new JPanel(new BorderLayout());
206            final JComponent overlay = createTestOverlay();
207            final JComponent someField = createTestComponent();
208    
209            OverlayHelper.attachOverlay(overlay, someField, 0, 0, 0);
210    
211            view.add(someField);
212    
213            final JScrollPane scrollPane = new JScrollPane(view);
214    
215            waitUntilEventQueueIsEmpty();
216    
217            final Component viewportView = scrollPane.getViewport().getView();
218    //        assertFalse(viewportView == view);
219            assertFalse(viewportView instanceof Scrollable);
220        }
221    
222        public void testSwapScrollableForNonScrollable() throws Exception {
223            JComponent view = new ScrollablePanel(new BorderLayout());
224    
225            final JComponent overlay = createTestOverlay();
226            final JComponent someField = createTestComponent();
227    
228            OverlayHelper.attachOverlay(overlay, someField, 0, 0, 0);
229    
230            view.add(someField);
231    
232            final JScrollPane scrollPane = new JScrollPane(view);
233    
234            waitUntilEventQueueIsEmpty();
235    
236            Component viewportView = scrollPane.getViewport().getView();
237    //        assertFalse(viewportView == view);
238            assertTrue(viewportView instanceof Scrollable);
239    
240            view.remove(someField);
241            view = new JPanel(new BorderLayout());
242            view.add(someField);
243            scrollPane.setViewportView(view);
244    
245            waitUntilEventQueueIsEmpty();
246    
247            viewportView = scrollPane.getViewport().getView();
248    //        assertFalse(viewportView == view);
249            assertFalse(viewportView instanceof Scrollable);
250    
251            view.remove(someField);
252            view = new ScrollablePanel(new BorderLayout());
253            view.add(someField);
254            scrollPane.setViewportView(view);
255    
256            waitUntilEventQueueIsEmpty();
257    
258            viewportView = scrollPane.getViewport().getView();
259    //        assertFalse(viewportView == view);
260            assertTrue(viewportView instanceof Scrollable);
261        }
262    
263        private JComponent createTestComponent() {
264            return new JTextField("Hello, world!") {
265                // This is to force the OverlayHelper to install the overlay,
266                // even though we don't have a UI visible.
267                public boolean isVisible() {
268                    return true;
269                }
270    
271                public boolean isShowing() {
272                    return true;
273                }
274            };
275        }
276    
277        private JComponent createTestOverlay() {
278            final JComponent overlay = new JLabel("x") {
279                // This is to force the OverlayHelper to install the overlay,
280                // even though we don't have a UI visible.
281                public boolean isVisible() {
282                    return true;
283                }
284    
285                public boolean isShowing() {
286                    return true;
287                }
288            };
289            overlay.setOpaque(false);
290            return overlay;
291        }
292    
293        public static class ScrollablePanel extends JPanel implements Scrollable {
294            private int scrollableUnitIncrement = 10;
295    
296            private int scrollableBlockIncrement = 40;
297    
298            private boolean scrollableTracksViewportWidth = false;
299    
300            private boolean scrollableTracksViewportHeight = false;
301    
302            public ScrollablePanel(LayoutManager layout, boolean isDoubleBuffered) {
303                super(layout, isDoubleBuffered);
304            }
305    
306            public ScrollablePanel(LayoutManager layout) {
307                super(layout);
308            }
309    
310            public ScrollablePanel(boolean isDoubleBuffered) {
311                super(isDoubleBuffered);
312            }
313    
314            public ScrollablePanel() {
315            }
316    
317            public void setScrollableUnitIncrement(final int scrollableUnitIncrement) {
318                this.scrollableUnitIncrement = scrollableUnitIncrement;
319            }
320    
321            public void setScrollableBlockIncrement(final int scrollableBlockIncrement) {
322                this.scrollableBlockIncrement = scrollableBlockIncrement;
323            }
324    
325            public void setScrollableTracksViewportWidth(final boolean scrollableTracksViewportWidth) {
326                this.scrollableTracksViewportWidth = scrollableTracksViewportWidth;
327            }
328    
329            public void setScrollableTracksViewportHeight(final boolean scrollableTracksViewportHeight) {
330                this.scrollableTracksViewportHeight = scrollableTracksViewportHeight;
331            }
332    
333            public Dimension getPreferredScrollableViewportSize() {
334                return getPreferredSize();
335            }
336    
337            public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
338                return this.scrollableUnitIncrement;
339            }
340    
341            public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
342                return this.scrollableBlockIncrement;
343            }
344    
345            public boolean getScrollableTracksViewportWidth() {
346                return this.scrollableTracksViewportWidth;
347            }
348    
349            public boolean getScrollableTracksViewportHeight() {
350                return this.scrollableTracksViewportHeight;
351            }
352        }
353    }