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