1   /*
2    * Copyright 2002-2004 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    * 
8    * http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  package org.springframework.richclient.util;
17  
18  import java.awt.AWTEvent;
19  import java.awt.BorderLayout;
20  import java.awt.Component;
21  import java.awt.Dimension;
22  import java.awt.EventQueue;
23  import java.awt.LayoutManager;
24  import java.awt.Rectangle;
25  import java.awt.Toolkit;
26  import java.lang.ref.ReferenceQueue;
27  import java.lang.ref.PhantomReference;
28  import java.lang.reflect.InvocationTargetException;
29  import java.util.concurrent.FutureTask;
30  import java.util.concurrent.Callable;
31  
32  import javax.swing.JComponent;
33  import javax.swing.JLabel;
34  import javax.swing.JPanel;
35  import javax.swing.JScrollPane;
36  import javax.swing.JTextField;
37  import javax.swing.Scrollable;
38  import javax.swing.JRootPane;
39  import javax.swing.JLayeredPane;
40  import javax.swing.SwingUtilities;
41  
42  import org.springframework.richclient.test.SpringRichTestCase;
43  
44  /**
45   * @author andy
46   * @since May 12, 2006 8:32:27 AM
47   */
48  public class OverlayHelperTests extends SpringRichTestCase {
49  
50      /**
51       * OverlayHelper installs the overlay as the View of a JScrollPane viewport, if the component is in a JScrollPane,
52       * so that the overlay is shown in the proper location when scrolled. However, to accomplish this, it will remove
53       * the component that was in the viewport, add it to a JLayeredPane, and then add that JLayeredPane to the viewport
54       * instead. This introduced a bug if the viewport's view happened to implement the Scrollable interface, since
55       * JScrollPane does <i>not</i> implement the Scrollable interface. See issue RCP-344.
56       * 
57       * @throws Exception
58       */
59      public void testRegressionScrollableProxy() throws Exception {
60          performScrollableTest();
61          performNonScrollableTest();
62      }
63  
64  
65      /**
66       * See <a href="http://opensource.atlassian.com/projects/spring/browse/RCP-492">RCP-492<a/>
67       * for details on this test and related bug.
68       *
69       * @throws Exception
70       */
71      public void testRegressionOverlayHelperLeak() throws Exception {
72          // Basically, this test will simulate adding a component to a
73          // JRootPane, then attach an overlay to it.  The overlay will be
74          // installed into the layered pane of the JRootPane at the
75          // PALETTE_LAYER.  This test will then remove the component from the
76          // JRootPane, ensure the overlay is removed from the layered pane,
77          // and then further ensure that nothing else holds a strong reference
78          // to the component (by waiting for the component to be garbage
79          // collected).
80          JComponent component = createTestComponent();
81          JComponent overlay = createTestOverlay();
82          final ReferenceQueue rq = new ReferenceQueue();
83          final PhantomReference componentRef = new PhantomReference(component, rq);
84  
85          final JRootPane rootPane = new JRootPane() {
86              public boolean isVisible() {
87                  return true;
88              }
89              public boolean isShowing() {
90                  return true;
91              }
92              protected JLayeredPane createLayeredPane() {
93                  return new JLayeredPane() {
94                      public boolean isVisible() {
95                          return true;
96                      }
97                      public boolean isShowing() {
98                          return true;
99                      }
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 }