1/*
2 * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23package org.netbeans.jemmy;
24
25import java.awt.AWTEvent;
26import java.awt.EventQueue;
27import java.awt.Toolkit;
28import java.awt.event.InvocationEvent;
29import java.lang.reflect.InvocationTargetException;
30
31/**
32 *
33 * Provides functionality to work with java.awt.EventQueue empty.
34 *
35 * <BR><BR>Timeouts used: <BR>
36 * QueueTool.WaitQueueEmptyTimeout - timeout to wait queue emptied<BR>
37 * QueueTool.QueueCheckingDelta - time delta to check result<BR>
38 * QueueTool.LockTimeout - time to wait queue locked after lock action has been
39 * put there<BR>
40 * QueueTool.InvocationTimeout - time for action was put into queue to be
41 * started<BR>
42 * QueueTool.MaximumLockingTime - maximum time to lock queue.<br>
43 *
44 * @see Timeouts
45 *
46 * @author Alexandre Iline (alexandre.iline@oracle.com)
47 *
48 */
49public class QueueTool implements Outputable, Timeoutable {
50
51    private final static long WAIT_QUEUE_EMPTY_TIMEOUT = 180000;
52    private final static long QUEUE_CHECKING_DELTA = 10;
53    private final static long LOCK_TIMEOUT = 180000;
54    private final static long MAXIMUM_LOCKING_TIME = 180000;
55    private final static long INVOCATION_TIMEOUT = 180000;
56
57    private static JemmyQueue jemmyQueue = null;
58
59    private TestOut output;
60    private Timeouts timeouts;
61    private Locker locker;
62    private Waiter<String, Void> lockWaiter;
63
64    /**
65     * Constructor.
66     */
67    public QueueTool() {
68        locker = new Locker();
69        lockWaiter = new Waiter<String, Void>(new Waitable<String, Void>() {
70            @Override
71            public String actionProduced(Void obj) {
72                return locker.isLocked() ? "" : null;
73            }
74
75            @Override
76            public String getDescription() {
77                return "Event queue to be locked";
78            }
79
80            @Override
81            public String toString() {
82                return "QueueTool.Waiter{" + getDescription() + '}';
83            }
84        });
85        setOutput(JemmyProperties.getProperties().getOutput());
86        setTimeouts(JemmyProperties.getProperties().getTimeouts());
87    }
88
89    /**
90     * Returns system EventQueue.
91     *
92     * @return system EventQueue.
93     */
94    public static EventQueue getQueue() {
95        return Toolkit.getDefaultToolkit().getSystemEventQueue();
96    }
97
98    /**
99     * Map to {@code EventQueue.isDispatchThread()}.
100     *
101     * @return true if this thread is the AWT dispatching thread.
102     */
103    public static boolean isDispatchThread() {
104        return EventQueue.isDispatchThread();
105    }
106
107    /**
108     * Checks if system event queue is empty.
109     *
110     * @return true if EventQueue is empty.
111     */
112    public static boolean checkEmpty() {
113        return getQueue().peekEvent() == null;
114    }
115
116    /**
117     * Shortcuts event if
118     * {@code ((JemmyProperties.getCurrentDispatchingModel() & JemmyProperties.SHORTCUT_MODEL_MASK) != 0)}
119     * and if executed in the dispatch thread. Otherwise posts event.
120     *
121     * @param event Event to dispatch.
122     */
123    public static void processEvent(AWTEvent event) {
124        if ((JemmyProperties.getCurrentDispatchingModel()
125                & JemmyProperties.SHORTCUT_MODEL_MASK) != 0) {
126            installQueue();
127        }
128        if ((JemmyProperties.getCurrentDispatchingModel()
129                & JemmyProperties.SHORTCUT_MODEL_MASK) != 0
130                && isDispatchThread()) {
131            shortcutEvent(event);
132        } else {
133            postEvent(event);
134        }
135    }
136
137    /**
138     * Simply posts events into the system event queue.
139     *
140     * @param event Event to dispatch.
141     */
142    public static void postEvent(AWTEvent event) {
143        getQueue().postEvent(event);
144    }
145
146    /**
147     * Dispatches event ahead of all events staying in the event queue.
148     *
149     * @param event an event to be shortcut.
150     */
151    public static void shortcutEvent(AWTEvent event) {
152        installQueue();
153        jemmyQueue.shortcutEvent(event);
154    }
155
156    /**
157     * Installs own Jemmy EventQueue implementation. The method is executed in
158     * dispatchmode only.
159     *
160     * @see #uninstallQueue
161     */
162    public static void installQueue() {
163        if (jemmyQueue == null) {
164            jemmyQueue = new JemmyQueue();
165        }
166        jemmyQueue.install();
167    }
168
169    /**
170     * Uninstalls own Jemmy EventQueue implementation.
171     *
172     * @see #installQueue
173     */
174    public static void uninstallQueue() {
175        if (jemmyQueue != null) {
176            jemmyQueue.uninstall();
177        }
178    }
179
180    static {
181        Timeouts.initDefault("QueueTool.WaitQueueEmptyTimeout", WAIT_QUEUE_EMPTY_TIMEOUT);
182        Timeouts.initDefault("QueueTool.QueueCheckingDelta", QUEUE_CHECKING_DELTA);
183        Timeouts.initDefault("QueueTool.LockTimeout", LOCK_TIMEOUT);
184        Timeouts.initDefault("QueueTool.InvocationTimeout", INVOCATION_TIMEOUT);
185        Timeouts.initDefault("QueueTool.MaximumLockingTime", MAXIMUM_LOCKING_TIME);
186    }
187
188    /**
189     * Defines current timeouts.
190     *
191     * @param ts ?t? A collection of timeout assignments.
192     * @see org.netbeans.jemmy.Timeouts
193     * @see org.netbeans.jemmy.Timeoutable
194     * @see #getTimeouts
195     */
196    @Override
197    public void setTimeouts(Timeouts ts) {
198        timeouts = ts;
199        lockWaiter.setTimeouts(getTimeouts().cloneThis());
200    }
201
202    /**
203     * Return current timeouts.
204     *
205     * @return the collection of current timeout assignments.
206     * @see org.netbeans.jemmy.Timeouts
207     * @see org.netbeans.jemmy.Timeoutable
208     * @see #setTimeouts
209     */
210    @Override
211    public Timeouts getTimeouts() {
212        return timeouts;
213    }
214
215    /**
216     * Defines print output streams or writers.
217     *
218     * @param out Identify the streams or writers used for print output.
219     * @see org.netbeans.jemmy.Outputable
220     * @see org.netbeans.jemmy.TestOut
221     * @see #getOutput
222     */
223    @Override
224    public void setOutput(TestOut out) {
225        output = out;
226        lockWaiter.setOutput(output.createErrorOutput());
227    }
228
229    /**
230     * Returns print output streams or writers.
231     *
232     * @return an object that contains references to objects for printing to
233     * output and err streams.
234     * @see org.netbeans.jemmy.Outputable
235     * @see org.netbeans.jemmy.TestOut
236     * @see #setOutput
237     */
238    @Override
239    public TestOut getOutput() {
240        return output;
241    }
242
243    /**
244     * Waits for system event queue empty. Uses
245     * "QueueTool.WaitQueueEmptyTimeout" milliseconds to wait.
246     *
247     * @throws TimeoutExpiredException
248     */
249    public void waitEmpty() {
250        Waiter<String, Void> waiter = new Waiter<>(new Waitable<String, Void>() {
251            @Override
252            public String actionProduced(Void obj) {
253                if (checkEmpty()) {
254                    return "Empty";
255                }
256                return null;
257            }
258
259            @Override
260            public String getDescription() {
261                return "Wait event queue empty";
262            }
263
264            @Override
265            public String toString() {
266                return "waitEmpty.Waiter{" + getDescription() + '}';
267            }
268        });
269        waiter.setTimeoutsToCloneOf(timeouts, "QueueTool.WaitQueueEmptyTimeout");
270        waiter.setOutput(output);
271        try {
272            waiter.waitAction(null);
273        } catch (TimeoutExpiredException e) {
274            final AWTEvent event = getQueue().peekEvent();
275            // if event != null run toString in dispatch thread
276            String eventToString = (event == null) ? "null" : invokeSmoothly(
277                    new QueueTool.QueueAction<String>("event.toString()") {
278                @Override
279                public String launch() {
280                    return event.toString();
281                }
282            }
283            );
284            getOutput().printErrLine("Event at the top of stack: " + eventToString);
285            throw (e);
286        } catch (InterruptedException e) {
287            output.printStackTrace(e);
288        }
289    }
290
291    /**
292     * Waits for system event queue be empty for {@code emptyTime}
293     * milliseconds. Uses "QueueTool.WaitQueueEmptyTimeout" milliseconds to
294     * wait.
295     *
296     * @param emptyTime time for the queue to stay empty.
297     * @throws TimeoutExpiredException
298     */
299    public void waitEmpty(long emptyTime) {
300
301        StayingEmptyWaiter waiter = new StayingEmptyWaiter(emptyTime);
302        waiter.setTimeoutsToCloneOf(timeouts, "QueueTool.WaitQueueEmptyTimeout");
303        waiter.setOutput(output);
304        try {
305            waiter.waitAction(null);
306        } catch (TimeoutExpiredException e) {
307            final AWTEvent event = getQueue().peekEvent();
308            String eventToString = (event == null) ? "null" : invokeSmoothly(
309                    new QueueTool.QueueAction<String>("event.toString()") {
310                @Override
311                public String launch() {
312                    return event.toString();
313                }
314            }
315            );
316            getOutput().printErrLine("Event at the top of stack: " + eventToString);
317            throw (e);
318        } catch (InterruptedException e) {
319            output.printStackTrace(e);
320        }
321    }
322
323    /**
324     * Invokes action through EventQueue. Does not wait for it execution.
325     *
326     * @param action an action to be invoked.
327     */
328    public void invoke(QueueAction<?> action) {
329        output.printTrace("Invoking \"" + action.getDescription() + "\" action through event queue");
330        EventQueue.invokeLater(action);
331    }
332
333    /**
334     * Invokes runnable through EventQueue. Does not wait for it execution.
335     *
336     * @param runnable a runnable to be invoked.
337     * @return QueueAction instance which can be use for execution monitoring.
338     * @see QueueTool.QueueAction
339     */
340    public QueueAction<Void> invoke(Runnable runnable) {
341        QueueAction<Void> result = new RunnableRunnable(runnable);
342        invoke(result);
343        return result;
344    }
345
346    /**
347     * Invokes action through EventQueue. Does not wait for it execution.
348     *
349     * @param action an action to be invoked.
350     * @param param {@code action.launch(Object)} method parameter.
351     * @return QueueAction instance which can be use for execution monitoring.
352     * @see QueueTool.QueueAction
353     */
354    public <R, P> QueueAction<R> invoke(Action<R, P> action, P param) {
355        QueueAction<R> result = new ActionRunnable<>(action, param);
356        invoke(result);
357        return result;
358    }
359
360    /**
361     * Being executed outside of AWT dispatching thread, invokes an action
362     * through the event queue. Otherwise executes {@code action.launch()}
363     * method directly.
364     *
365     * @param action anaction to be executed.
366     * @return Action result.
367     */
368    public <R> R invokeSmoothly(QueueAction<R> action) {
369        if (!EventQueue.isDispatchThread()) {
370            return invokeAndWait(action);
371        } else {
372            try {
373                return action.launch();
374            } catch (Exception e) {
375                throw (new JemmyException("Exception in " + action.getDescription(), e));
376            }
377        }
378    }
379
380    /**
381     * Being executed outside of AWT dispatching thread, invokes a runnable
382     * through the event queue. Otherwise executes {@code runnable.run()}
383     * method directly.
384     *
385     * @param runnable a runnable to be executed.
386     */
387    public void invokeSmoothly(Runnable runnable) {
388        if (!EventQueue.isDispatchThread()) {
389            invokeAndWait(runnable);
390        } else {
391            runnable.run();
392        }
393    }
394
395    /**
396     * Being executed outside of AWT dispatching thread, invokes an action
397     * through the event queue. Otherwise executes
398     * {@code action.launch(Object)} method directly.
399     *
400     * @param action anaction to be executed.
401     * @param param an action parameter
402     * @return Action result.
403     */
404    public <R, P> R invokeSmoothly(Action<R, P> action, P param) {
405        if (!EventQueue.isDispatchThread()) {
406            return invokeAndWait(action, param);
407        } else {
408            return action.launch(param);
409        }
410    }
411
412    /**
413     * Invokes action through EventQueue. Waits for it execution.
414     *
415     * @param action an action to be invoked.
416     * @return a result of action
417     * @throws TimeoutExpiredException if action was not executed in
418     * "QueueTool.InvocationTimeout" milliseconds.
419     */
420    public <R> R invokeAndWait(QueueAction<R> action) {
421
422        class JemmyInvocationLock {
423        }
424        Object lock = new JemmyInvocationLock();
425        InvocationEvent event
426                = new JemmyInvocationEvent(Toolkit.getDefaultToolkit(),
427                        action,
428                        lock,
429                        true);
430        try {
431            synchronized (lock) {
432                getQueue().postEvent(event);
433                while (!action.getFinished()) {
434                    lock.wait();
435                }
436            }
437        } catch (InterruptedException e) {
438            throw (new JemmyException("InterruptedException during "
439                    + action.getDescription()
440                    + " execution", e));
441        }
442        if (action.getException() != null) {
443            throw (new JemmyException("Exception in " + action.getDescription(),
444                    action.getException()));
445        }
446        if (event.getException() != null) {
447            throw (new JemmyException("Exception in " + action.getDescription(),
448                    event.getException()));
449        }
450        return action.getResult();
451    }
452
453    public static final class JemmyInvocationEvent extends InvocationEvent {
454
455        private static final long serialVersionUID = 42L;
456
457        public JemmyInvocationEvent(Object source, Runnable runnable,
458                Object notifier, boolean catchThrowables) {
459            super(source, runnable, notifier, catchThrowables);
460        }
461    }
462
463    /**
464     * Invokes runnable through EventQueue. Waits for it execution.
465     *
466     * @param runnable a runnable to be invoked.
467     * @throws TimeoutExpiredException if runnable was not executed in
468     * "QueueTool.InvocationTimeout" milliseconds.
469     */
470    public void invokeAndWait(Runnable runnable) {
471        invokeAndWait(new RunnableRunnable(runnable));
472    }
473
474    /**
475     * Invokes action through EventQueue. Waits for it execution. May throw
476     * TimeoutExpiredException if action was not executed in
477     * "QueueTool.InvocationTimeout" milliseconds.
478     *
479     * @param action an action to be invoked.
480     * @param param action.launch(Object method parameter.
481     * @return a result of action
482     * @throws TimeoutExpiredException if action was not executed in
483     * "QueueTool.InvocationTimeout" milliseconds.
484     */
485    public <R, P> R invokeAndWait(Action<R, P> action, P param) {
486        return invokeAndWait(new ActionRunnable<>(action, param));
487    }
488
489    /**
490     * Locks EventQueue. Locking will be automatically aborted after
491     * "QueueTool.MaximumLockingTime" milliseconds.
492     *
493     * @see #unlock()
494     * @throws TimeoutExpiredException
495     */
496    public void lock() {
497        output.printTrace("Locking queue.");
498        invoke(locker);
499        try {
500            lockWaiter.
501                    getTimeouts().
502                    setTimeout("Waiter.WaitingTime",
503                            timeouts.
504                            getTimeout("QueueTool.LockTimeout"));
505            lockWaiter.
506                    getTimeouts().
507                    setTimeout("Waiter.TimeDelta",
508                            timeouts.
509                            getTimeout("QueueTool.QueueCheckingDelta"));
510            lockWaiter.waitAction(null);
511        } catch (InterruptedException e) {
512            output.printStackTrace(e);
513        }
514    }
515
516    /**
517     * Unlocks EventQueue.
518     *
519     * @see #lock()
520     */
521    public void unlock() {
522        output.printTrace("Unlocking queue.");
523        locker.setLocked(false);
524    }
525
526    /**
527     * Locks event queue for "time" milliseconds. Returns immediately after
528     * locking.
529     *
530     * @param time a time to lock the queue for.
531     */
532    public void lock(long time) {
533        output.printTrace("Locking queue for " + Long.toString(time) + " milliseconds");
534        lock();
535        invoke(new UnlockPostponer(time));
536    }
537
538    /**
539     * Sais if last locking was expired.
540     *
541     * @return true if last locking had beed expired.
542     */
543    public boolean wasLockingExpired() {
544        return locker.expired;
545    }
546
547    /**
548     * Action to be executed through event queue. Even if it was executed without
549     * waiting by {@code invoke(QueueAction)} execution process can be
550     * monitored by {@code getResult()}, {@code getException()},
551     * {@code getFinished()} methods.
552     */
553    public static abstract class QueueAction<R> implements Runnable {
554
555        private volatile boolean finished;
556        private Exception exception;
557        private R result;
558        private String description;
559
560        /**
561         * Constructor.
562         *
563         * @param description a description.
564         */
565        public QueueAction(String description) {
566            this.description = description;
567            finished = false;
568            exception = null;
569            result = null;
570        }
571
572        /**
573         * Method to implement action functionality.
574         *
575         * @return an Object - action result
576         * @throws Exception
577         */
578        public abstract R launch()
579                throws Exception;
580
581        /**
582         */
583        @Override
584        public final void run() {
585            finished = false;
586            exception = null;
587            result = null;
588            try {
589                result = launch();
590            } catch (Exception e) {
591                exception = e;
592            } finally {
593                finished = true;
594            }
595        }
596
597        /**
598         * Action description.
599         *
600         * @return the description.
601         */
602        public String getDescription() {
603            return description;
604        }
605
606        /**
607         * Returns action result if action has already been finished, null
608         * otherwise.
609         *
610         * @return an action result.
611         */
612        public R getResult() {
613            return result;
614        }
615
616        /**
617         * Returns exception occured during action execution (if any).
618         *
619         * @return the Exception happened inside {@code launch()} method.
620         */
621        public Exception getException() {
622            return exception;
623        }
624
625        /**
626         * Informs whether action has been finished or not.
627         *
628         * @return true if this action have been finished
629         */
630        public boolean getFinished() {
631            return finished;
632        }
633
634        @Override
635        public String toString() {
636            return "QueueAction{description=" + description + ", result=" + result + ", finished=" + finished + ", exception=" + exception + '}';
637        }
638    }
639
640    private static class JemmyQueue extends EventQueue {
641
642        private boolean installed = false;
643
644        public void shortcutEvent(AWTEvent event) {
645            super.dispatchEvent(event);
646        }
647
648        @Override
649        protected void dispatchEvent(AWTEvent event) {
650            //it's necessary to catch exception here.
651            //because test might already fail by timeout
652            //but generated events are still in stack
653            try {
654                super.dispatchEvent(event);
655            } catch (Exception e) {
656                //the exceptions should be printed into
657                //Jemmy output - not System.out
658                JemmyProperties.getCurrentOutput().printStackTrace(e);
659            }
660        }
661
662        public synchronized void install() {
663            if (!installed) {
664                getQueue().push(this);
665                installed = true;
666            }
667        }
668
669        public synchronized void uninstall() {
670            if (installed) {
671                pop();
672                installed = false;
673            }
674        }
675    }
676
677    private class EventWaiter implements Runnable {
678
679        boolean empty = true;
680        long emptyTime;
681
682        public EventWaiter(long emptyTime) {
683            this.emptyTime = emptyTime;
684        }
685
686        @Override
687        public void run() {
688            long startTime = System.currentTimeMillis();
689            while ((empty = checkEmpty())
690                    && (System.currentTimeMillis() - startTime) < emptyTime) {
691                timeouts.sleep("QueueTool.QueueCheckingDelta");
692            }
693        }
694    }
695
696    private class StayingEmptyWaiter extends Waiter<String, Void> {
697
698        long emptyTime;
699
700        public StayingEmptyWaiter(long emptyTime) {
701            this.emptyTime = emptyTime;
702        }
703
704        @Override
705        public String actionProduced(Void obj) {
706            try {
707                EventWaiter eventWaiter = new EventWaiter(emptyTime);
708                EventQueue.invokeAndWait(eventWaiter);
709                if (eventWaiter.empty
710                        && timeFromStart() <= super.getTimeouts().getTimeout("Waiter.WaitingTime")) {
711                    return "Reached";
712                }
713            } catch (InterruptedException | InvocationTargetException e) {
714                output.printStackTrace(e);
715            }
716            return null;
717        }
718
719        @Override
720        public String getDescription() {
721            return "Wait event queue staying empty for " + emptyTime;
722        }
723
724        @Override
725        public String toString() {
726            return "StayingEmptyWaiter{" + "emptyTime=" + emptyTime + '}';
727        }
728    }
729
730    private class ActionRunnable<R, P> extends QueueAction<R> {
731
732        Action<R, P> action;
733        P param;
734
735        public ActionRunnable(Action<R, P> action, P param) {
736            super(action.getDescription());
737            this.action = action;
738            this.param = param;
739        }
740
741        @Override
742        public R launch() throws Exception {
743            return action.launch(param);
744        }
745    }
746
747    private class RunnableRunnable extends QueueAction<Void> {
748
749        Runnable action;
750
751        public RunnableRunnable(Runnable action) {
752            super("Runnable");
753            this.action = action;
754        }
755
756        @Override
757        public Void launch() throws Exception {
758            action.run();
759            return null;
760        }
761    }
762
763    private class Locker extends QueueAction<Void> {
764
765        volatile boolean locked = false;
766        long wholeTime, deltaTime;
767        boolean expired;
768
769        public Locker() {
770            super("Event queue locking");
771        }
772
773        @Override
774        public Void launch() {
775            wholeTime = timeouts.getTimeout("QueueTool.MaximumLockingTime");
776            deltaTime = timeouts.getTimeout("QueueTool.QueueCheckingDelta");
777            setLocked(true);
778            expired = false;
779            long startTime = System.currentTimeMillis();
780            while (isLocked()) {
781                try {
782                    Thread.sleep(deltaTime);
783                } catch (InterruptedException e) {
784                    getOutput().printStackTrace(e);
785                }
786                if (System.currentTimeMillis() - startTime > wholeTime) {
787                    getOutput().printLine("Locking has been expired!");
788                    expired = true;
789                    break;
790                }
791            }
792            return null;
793        }
794
795        public void setLocked(boolean locked) {
796            this.locked = locked;
797        }
798
799        public boolean isLocked() {
800            return locked;
801        }
802    }
803
804    private class UnlockPostponer implements Runnable {
805
806        long time;
807
808        public UnlockPostponer(long time) {
809            this.time = time;
810        }
811
812        @Override
813        public void run() {
814            new Timeout("", time).sleep();
815            unlock();
816        }
817    }
818}
819