001    
002    /*
003     * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: ContextApplication.java 310 2012-03-26 15:47:20Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.vaadin;
009    
010    import com.vaadin.Application;
011    import com.vaadin.terminal.Terminal;
012    import com.vaadin.terminal.gwt.server.HttpServletRequestListener;
013    import com.vaadin.ui.Window;
014    
015    import java.net.SocketException;
016    import java.util.EventObject;
017    import java.util.HashSet;
018    import java.util.List;
019    import java.util.concurrent.ExecutorService;
020    import java.util.concurrent.Executors;
021    import java.util.concurrent.Future;
022    import java.util.concurrent.TimeUnit;
023    
024    import javax.servlet.http.HttpServletRequest;
025    import javax.servlet.http.HttpServletResponse;
026    
027    import org.slf4j.Logger;
028    import org.slf4j.LoggerFactory;
029    
030    /**
031     * {@link Application} subclass that provides some basic infrastructure for Vaadin applications:
032     * <ul>
033     *  <li>Access to the currently running Vaadin application (via a {@link ContextApplication#get()})</li>
034     *  <li>A way to safely interact with a Vaadin application from a background thread (via {@link ContextApplication#invoke})</li>
035     *  <li>Support for Vaadin {@linkplain ContextApplication#addListener application close event notifications}</li>
036     *  <li>Displays any exceptions thrown in an overlay error window</li>
037     *  <li>A {@link Logger} to use</li>
038     * </ul>
039     *
040     * @since 1.0.134
041     */
042    @SuppressWarnings("serial")
043    public abstract class ContextApplication extends Application implements HttpServletRequestListener {
044    
045        /**
046         * Default notification linger time for error notifications (in milliseconds): Value is {@value}ms.
047         */
048        public static final int DEFAULT_NOTIFICATION_DELAY = 30000;
049    
050        private static final ThreadLocal<ContextApplication> CURRENT_APPLICATION = new ThreadLocal<ContextApplication>();
051        private static final ThreadLocal<HttpServletRequest> CURRENT_REQUEST = new ThreadLocal<HttpServletRequest>();
052        private static final ThreadLocal<HttpServletResponse> CURRENT_RESPONSE = new ThreadLocal<HttpServletResponse>();
053    
054        protected final Logger log = LoggerFactory.getLogger(this.getClass());
055    
056        private final HashSet<CloseListener> closeListeners = new HashSet<CloseListener>();
057    
058        private volatile ExecutorService executorService;
059    
060    // Initialization
061    
062        /**
063         * Initialize the application.
064         *
065         * <p>
066         * The implementation in {@link ContextApplication} delegates to {@link #initApplication}.
067         * </p>
068         */
069        @Override
070        public final void init() {
071    
072            // Set current application
073            CURRENT_APPLICATION.set(this);
074    
075            // Create executor service
076            this.executorService = Executors.newSingleThreadExecutor();
077    
078            // Initialize application
079            boolean initialized = false;
080            try {
081                this.initApplication();
082                initialized = true;
083            } finally {
084                if (!initialized)
085                    this.shutdownExecutorService();
086            }
087        }
088    
089        /**
090         * Initialize the application. Sub-classes of {@link ContextApplication} must implement this method.
091         */
092        protected abstract void initApplication();
093    
094    // Error handling
095    
096        /**
097         * Handle an uncaugt exception thrown by a Vaadin HTTP request.
098         *
099         * <p>
100         * The implementation in {@link ContextApplication} logs the error and displays in on the
101         * user's screen via {@link #showError(String, Throwable)}.
102         */
103        @Override
104        public void terminalError(Terminal.ErrorEvent event) {
105    
106            // Delegate to superclass
107            super.terminalError(event);
108    
109            // Get exception; ignore client "hangups"
110            final Throwable t = event.getThrowable();
111            if (t instanceof SocketException)
112                return;
113    
114            // Notify user and log it
115            this.showError("Internal Error", "" + t);
116            this.log.error("error within Vaadin operation", t);
117        }
118    
119        /**
120         * Display an error message to the user.
121         */
122        public void showError(String title, String description) {
123            Window.Notification notification = new Window.Notification(title, description, Window.Notification.TYPE_ERROR_MESSAGE);
124            notification.setStyleName("warning");
125            notification.setDelayMsec(this.getNotificationDelay());
126            this.getMainWindow().showNotification(notification);
127        }
128    
129        /**
130         * Display an error message to the user caused by an exception.
131         */
132        public void showError(String title, Throwable t) {
133            for (int i = 0; i < 100 && t.getCause() != null; i++)
134                t = t.getCause();
135            this.showError(title, this.getErrorMessage(t));
136        }
137    
138        /**
139         * Get the notification linger time for error notifications (in milliseconds).
140         *
141         * <p>
142         * The implementation in {@link ContextApplication} returns {@link #DEFAULT_NOTIFICATION_DELAY}.
143         */
144        protected int getNotificationDelay() {
145            return DEFAULT_NOTIFICATION_DELAY;
146        }
147    
148        /**
149         * Convert an exception into a displayable error message.
150         */
151        protected String getErrorMessage(Throwable t) {
152            return t.getClass().getSimpleName() + ": " + t.getMessage();
153        }
154    
155    // ThreadLocal stuff
156    
157        /**
158         * Set this instance as the "current application" (if not set already) while invoking the given callback.
159         *
160         * <p>
161         * This method is useful for situations in which non-Vaadin threads need to call into Vaadin code
162         * that expects to successfully retrieve the current application via {@link #currentApplication}.
163         * </p>
164         *
165         * <p>
166         * This method also synchronizes on this {@link ContextApplication} instance as required by Vaadin for thread safety.
167         * </p>
168         *
169         * @param action action to perform
170         * @throws IllegalStateException if a different {@link ContextApplication} is already set as the current application
171         *  associated with the current thread
172         * @see #invokeLater invokeLater()
173         */
174        public void invoke(Runnable action) {
175            final ContextApplication previous = ContextApplication.CURRENT_APPLICATION.get();
176            if (previous != null && previous != this)
177                throw new IllegalStateException("there is already a current application for this thread");
178            ContextApplication.CURRENT_APPLICATION.set(this);
179            try {
180                synchronized (this) {
181                    action.run();
182                }
183            } finally {
184                ContextApplication.CURRENT_APPLICATION.set(previous);
185            }
186        }
187    
188        /**
189         * Set this instance as the "current application" and invoke the given callback from within another thread.
190         *
191         * <p>
192         * This method functions like {@link #invoke invoke()} except that {@code action} will be invoked from within
193         * a separate thread dedicated to this application instance. This is useful to reduce Vaadin application lock
194         * contention, by performing Vaadin-related actions in a separate, dedicated thread. Actions are executed
195         * in the order they are given to this method.
196         * </p>
197         *
198         * <p>
199         * The returned {@link Future}'s {@link Future#get get()} method will return null upon successful completion.
200         * This method itself always returns immediately.
201         * </p>
202         *
203         * @param action action to perform
204         * @return a {@link Future} representing the pending results of {@code action}
205         * @throws IllegalStateException if this instance is not initialized or has been closed
206         * @see #invoke invoke()
207         */
208        public Future<?> invokeLater(Runnable action) {
209            ExecutorService executor = this.executorService;
210            if (executor == null)
211                throw new IllegalStateException("application instance is either not initialized or already closed");
212            return executor.submit(new LaterRunnable(action));
213        }
214    
215        /**
216         * Handle the start of a request.
217         *
218         * <p>
219         * The implementation in {@link ContextApplication} delegates to {@link #doOnRequestStart}.
220         * </p>
221         */
222        @Override
223        public final void onRequestStart(HttpServletRequest request, HttpServletResponse response) {
224            ContextApplication.CURRENT_APPLICATION.set(this);
225            ContextApplication.CURRENT_REQUEST.set(request);
226            ContextApplication.CURRENT_RESPONSE.set(response);
227            this.doOnRequestStart(request, response);
228        }
229    
230        /**
231         * Handle the end of a request.
232         *
233         * <p>
234         * The implementation in {@link ContextApplication} delegates to {@link #doOnRequestEnd}.
235         * </p>
236         */
237        @Override
238        public final void onRequestEnd(HttpServletRequest request, HttpServletResponse response) {
239            try {
240                this.doOnRequestEnd(request, response);
241            } finally {
242                ContextApplication.CURRENT_APPLICATION.remove();
243                ContextApplication.CURRENT_REQUEST.remove();
244                ContextApplication.CURRENT_RESPONSE.remove();
245            }
246        }
247    
248        /**
249         * Sub-class hook for handling the start of a request. This method is invoked by {@link #onRequestStart}.
250         *
251         * <p>
252         * The implementation in {@link ContextApplication} does nothing. Subclasses should override as necessary.
253         * </p>
254         */
255        protected void doOnRequestStart(HttpServletRequest request, HttpServletResponse response) {
256        }
257    
258        /**
259         * Sub-class hook for handling the end of a request. This method is invoked by {@link #onRequestEnd}.
260         *
261         * <p>
262         * The implementation in {@link ContextApplication} does nothing. Subclasses should override as necessary.
263         * </p>
264         */
265        protected void doOnRequestEnd(HttpServletRequest request, HttpServletResponse response) {
266        }
267    
268        /**
269         * Get the {@link ContextApplication} instance associated with the current thread.
270         *
271         * <p>
272         * If the current thread is handling a Vaadin HTTP request that is executing within an {@link ContextApplication} instance,
273         * or is executing within {@link #invoke}, then this method will return the associated {@link ContextApplication}.
274         * </p>
275         *
276         * @return the {@link ContextApplication} associated with the current thread, or {@code null} if the current thread
277         *  is not servicing a Vaadin web request or the current Vaadin {@link Application} is not an {@link ContextApplication}
278         *
279         * @see #invoke
280         */
281        public static ContextApplication currentApplication() {
282            return ContextApplication.CURRENT_APPLICATION.get();
283        }
284    
285        /**
286         * Get the {@link ContextApplication} associated with the current thread, cast to the desired type.
287         *
288         * @param type expected application type
289         * @return the {@link ContextApplication} associated with the current thread
290         * @see #currentApplication()
291         * @throws ClassCastException if the current application is not assignable to {@code type}
292         */
293        public static <A extends ContextApplication> A currentApplication(Class<A> type) {
294            return type.cast(ContextApplication.currentApplication());
295        }
296    
297        /**
298         * Get the {@link ContextApplication} instance associated with the current thread or throw an exception if there is none.
299         *
300         * <p>
301         * If the current thread is handling a Vaadin web request that is executing within an {@link ContextApplication} instance,
302         * or is executing within {@link #invoke}, then this method will return the associated {@link ContextApplication}.
303         * Otherwise, an exception is thrown.
304         * </p>
305         *
306         * @return the {@link ContextApplication} associated with the current thread
307         * @throws IllegalStateException if the current thread is not servicing a Vaadin web request
308         *  or the current Vaadin {@link Application} is not an {@link ContextApplication}
309         */
310        public static ContextApplication get() {
311            ContextApplication app = ContextApplication.currentApplication();
312            if (app != null)
313                return app;
314            throw new IllegalStateException("no current application found");
315        }
316    
317        /**
318         * Get the {@link ContextApplication} instance associated with the current thread, cast to the desired type,
319         * or throw an exception if there is none.
320         *
321         * @param type expected application type
322         * @return the {@link ContextApplication} associated with the current thread
323         * @throws IllegalStateException if the current {@link ContextApplication} is not found
324         * @see #get()
325         * @throws ClassCastException if the current application is not assignable to {@code type}
326         */
327        public static <A extends ContextApplication> A get(Class<A> type) {
328            A app = ContextApplication.currentApplication(type);
329            if (app != null)
330                return app;
331            throw new IllegalStateException("no current application found");
332        }
333    
334        /**
335         * Get the {@link HttpServletRequest} associated with the current thread.
336         *
337         * <p>
338         * If the current thread is handling a Vaadin web request for an instance of this class,
339         * this method will return the associated {@link HttpServletRequest}.
340         * </p>
341         *
342         * @return the {@link HttpServletRequest} associated with the current thread, or {@code null} if the current thread
343         *  is not servicing a Vaadin web request or the current Vaadin {@link Application} is not an instance of this class
344         */
345        public static HttpServletRequest currentRequest() {
346            return ContextApplication.CURRENT_REQUEST.get();
347        }
348    
349        /**
350         * Get the {@link HttpServletResponse} associated with the current thread.
351         *
352         * <p>
353         * If the current thread is handling a Vaadin web request for an instance of this class,
354         * this method will return the associated {@link HttpServletResponse}.
355         * </p>
356         *
357         * @return the {@link HttpServletResponse} associated with the current thread, or {@code null} if the current thread
358         *  is not servicing a Vaadin web request or the current Vaadin {@link Application} is not an instance of this class
359         */
360        public static HttpServletResponse currentResponse() {
361            return ContextApplication.CURRENT_RESPONSE.get();
362        }
363    
364    // Listener stuff
365    
366        /**
367         * Close this instance.
368         *
369         * <p>
370         * The implementation in {@link ContextApplication} first delegates to the superclass and then
371         * notifies any registered {@link CloseListener}s.
372         * </p>
373         */
374        @Override
375        public void close() {
376    
377            // Invoke superclass
378            super.close();
379    
380            // Notify listeners
381            CloseEvent closeEvent = new CloseEvent(this);
382            for (CloseListener closeListener : this.getCloseListeners()) {
383                try {
384                    closeListener.applicationClosed(closeEvent);
385                } catch (ThreadDeath t) {
386                    throw t;
387                } catch (Throwable t) {
388                    this.log.error("exception thrown by CloseListener " + closeListener, t);
389                }
390            }
391    
392            // Shutdown ExecutorService
393            this.shutdownExecutorService();
394        }
395    
396        private void shutdownExecutorService() {
397    
398            // Already shutdown?
399            if (this.executorService == null)
400                return;
401    
402            // Shut it down; waiting up to 1 second to finish
403            this.executorService.shutdown();
404            boolean terminated = false;
405            try {
406                terminated = this.executorService.awaitTermination(1, TimeUnit.SECONDS);
407            } catch (InterruptedException e) {
408                // ok, give up
409            }
410    
411            // Log warnings if it didn't finish
412            if (!terminated) {
413                this.log.warn("forcibly terminating outstanding tasks for closed Vaadin application " + this);
414                List<Runnable> list = this.executorService.shutdownNow();
415                if (!list.isEmpty())
416                    this.log.warn(list.size() + " outstanding task(s) remain for closed Vaadin application " + this + ": " + list);
417            }
418    
419            // Done
420            this.executorService = null;
421        }
422    
423        /**
424         * Add a {@link CloseListener} to be notified when this instance is closed.
425         */
426        public void addListener(CloseListener listener) {
427            synchronized (this.closeListeners) {
428                this.closeListeners.add(listener);
429            }
430        }
431    
432        /**
433         * Remove a {@link CloseListener}.
434         */
435        public void removeListener(CloseListener listener) {
436            synchronized (this.closeListeners) {
437                this.closeListeners.remove(listener);
438            }
439        }
440    
441        private HashSet<CloseListener> getCloseListeners() {
442            synchronized (this.closeListeners) {
443                return new HashSet<CloseListener>(this.closeListeners);
444            }
445        }
446    
447        /**
448         * Implemented by listeners that wish to be notified when a {@link ContextApplication} is closed.
449         *
450         * @see ContextApplication#addListener
451         */
452        public interface CloseListener {
453    
454            /**
455             * Notification upon {@link ContextApplication} closing.
456             */
457            void applicationClosed(CloseEvent closeEvent);
458        }
459    
460        /**
461         * Event delivered to listeners when a {@link ContextApplication} is closed.
462         */
463        public static class CloseEvent extends EventObject {
464    
465            public CloseEvent(ContextApplication application) {
466                super(application);
467            }
468    
469            /**
470             * Get the {@link ContextApplication} that is being closed.
471             */
472            public ContextApplication getContextApplication() {
473                return (ContextApplication)super.getSource();
474            }
475        }
476    
477        // Used by invokeLater()
478        private class LaterRunnable implements Runnable {
479    
480            private final Runnable action;
481    
482            LaterRunnable(Runnable action) {
483                this.action = action;
484            }
485    
486            @Override
487            public void run() {
488                try {
489                    ContextApplication.this.invoke(this.action);
490                } catch (ThreadDeath t) {
491                    throw t;
492                } catch (Throwable e) {
493                    ContextApplication.this.log.error("exception thrown by invokeLater() action " + this.action, e);
494                }
495            }
496    
497            @Override
498            public String toString() {
499                return this.action.toString();
500            }
501        }
502    }
503