001 package com.sptci.echo2;
002
003 import java.lang.reflect.InvocationTargetException;
004 import java.util.Arrays;
005 import java.util.Date;
006 import java.util.Locale;
007 import java.util.Map;
008 import java.util.HashMap;
009 import java.util.TimeZone;
010
011 import java.util.logging.Logger;
012 import java.util.logging.Level;
013
014 import nextapp.echo2.app.ApplicationInstance;
015 import nextapp.echo2.app.Component;
016 import nextapp.echo2.app.ContentPane;
017 import nextapp.echo2.app.FloatingPane;
018 import nextapp.echo2.app.StyleSheet;
019 import nextapp.echo2.app.TaskQueueHandle;
020 import nextapp.echo2.app.Window;
021
022 import nextapp.echo2.webcontainer.ContainerContext;
023 import nextapp.echo2.webrender.ClientProperties;
024
025 import com.sptci.ReflectionUtility;
026 import com.sptci.auth.User;
027
028 /**
029 * A base class that extends <code>ApplicationInstance</code> and
030 * can serve as the base class for the application specific
031 * global application instance.
032 *
033 * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
034 * @author Rakesh Vidyadharan 2006-11-23
035 * @version $Id: Application.java 3321 2007-06-07 17:53:13Z rakesh $
036 */
037 public class Application extends ApplicationInstance
038 {
039 /**
040 * The logger to use to log errors/messages to.
041 */
042 protected static final Logger logger =
043 Logger.getLogger( Application.class.getName() );
044
045 /**
046 * The minimum interval at which tasks will be enqueued.
047 */
048 public static final int MINIMUM_INTERVAL = 2000;
049
050 /**
051 * A map that is used to store the time offsets from UTC and their
052 * corresponding time zone short names.
053 */
054 public static final Map<String,TimeZone> TIME_ZONES;
055
056 /**
057 * Static initialiser for {@link #TIME_ZONES}.
058 */
059 static
060 {
061 TIME_ZONES = new HashMap<String,TimeZone>();
062 Date date = new Date();
063 for ( String id : TimeZone.getAvailableIDs() )
064 {
065 TimeZone timeZone = TimeZone.getTimeZone( id );
066 TIME_ZONES.put( getOffset( timeZone.getRawOffset() ), timeZone );
067 }
068 }
069
070 /**
071 * The task queue handle for this application instance.
072 */
073 private TaskQueueHandle taskQueue;
074
075 /**
076 * A container used to store all the tasks that have been queued for the
077 * current application session.
078 */
079 private Map<Controller.Updater,Runnable> updateTasks =
080 new HashMap<Controller.Updater,Runnable>();
081
082 /**
083 * The polling interval as which any tasks in {@link #updateTasks} will
084 * be executed.
085 */
086 protected int pollingInterval;
087
088 /**
089 * The time zone for the client browser.
090 */
091 protected TimeZone timeZone;
092
093 /**
094 * The <code>Window</code> instance that will be controlled by this
095 * application.
096 */
097 protected Window window;
098
099 /**
100 * The fully qualified name of the <code>ContentPane</code> class that will
101 * be set as the content of {@link #window}.
102 */
103 protected String contentPane;
104
105 /**
106 * The fully qualified name of the <code>StyleSheet</code> class that will be
107 * set for the application.
108 */
109 protected String styleSheet;
110
111 /**
112 * The value object that represents the user currently logged on to the
113 * application.
114 */
115 protected User user;
116
117 /**
118 * Convenience method to return the active application as a properly typed
119 * object.
120 *
121 * @return The currently active <code>Application</code> instance.
122 */
123 public static Application getApplication()
124 {
125 return (Application) getActive();
126 }
127
128 /**
129 * Default constructor. No actions required.
130 */
131 public Application() {}
132
133 /**
134 * The mandatory initialisation method. Initialises {@link #window}
135 * and returns it.
136 *
137 * @see ServerDelayMessage
138 * @see #setStyleSheet
139 * @see nextapp.echo2.app.Window#setContent
140 * @return Window The properly initialised {@link #window}.
141 */
142 @Override
143 public Window init()
144 {
145 window = new Window();
146 window.setTitle(
147 Configuration.getString( "Application.Title.Window" ) );
148 try
149 {
150 ContainerContext containerContext = (ContainerContext)
151 getContextProperty( ContainerContext.CONTEXT_PROPERTY_NAME );
152 containerContext.setServerDelayMessage(
153 new ServerDelayMessage( getLocales() ) );
154 setStyleSheet( (StyleSheet) Class.forName( styleSheet ).newInstance() );
155 window.setContent( (ContentPane) Class.forName( contentPane ).newInstance() );
156 }
157 catch ( Throwable t )
158 {
159 logger.log( Level.SEVERE, "Error initialising ContentPane: "
160 + contentPane + " or StyleSheet: " + styleSheet, t );
161 processFatalException( t );
162 }
163
164 return window;
165 }
166
167 /**
168 * A global handler for exceptions encountered while updating the
169 * display.
170 *
171 * @param t The fatal exception
172 */
173 public void processFatalException( Throwable t )
174 {
175 logger.log( Level.SEVERE, "Unrecoverable error encountered.", t );
176 String message = ( t.getMessage() == null ) ? t.toString() : t.getMessage();
177 ErrorPane error = new ErrorPane( Configuration.getString(
178 "Application.FatalException.Title" ), message );
179 window.getContent().add( error );
180 }
181
182 /**
183 * Returns {@link #window}.
184 *
185 * @return Window The value/reference of/to window.
186 */
187 public Window getWindow()
188 {
189 return window;
190 }
191
192 /**
193 * Add the specified {@link nextapp.echo2.app.FloatingPane} to the
194 * {@link nextapp.echo2.app.ContentPane} of {@link #window} with an
195 * appropriate <code>z-index</code>.
196 *
197 * @see #getZIndex
198 * @param pane The window pane that is to be added.
199 */
200 public void addPane( FloatingPane pane )
201 {
202 try
203 {
204 ReflectionUtility.fetchMethod( pane, "setZIndex",
205 int.class ).invoke( pane, getZIndex() );
206 }
207 catch ( Throwable t )
208 {
209 logger.log( Level.SEVERE, "Error setting z-index for FloatingPane", t );
210 }
211
212 window.getContent().add( (Component) pane );
213 }
214
215 /**
216 * Remove the specified {@link nextapp.echo2.app.FloatingPane} from the
217 * {@link nextapp.echo2.app.ContentPane} of {@link #window}.
218 *
219 * @param pane The pane window pane that is to be removed.
220 */
221 public void removePane( FloatingPane pane )
222 {
223 window.getContent().remove( (Component) pane );
224 }
225
226 /**
227 * Get the <code>z-index</code> value to use for the next
228 * {@link nextapp.echo2.app.FloatingPane} component to be added.
229 */
230 protected int getZIndex()
231 {
232 int zIndex = 0;
233 for ( Component component : window.getContent().getComponents() )
234 {
235 if ( component instanceof FloatingPane )
236 {
237 int value = 0;
238
239 try
240 {
241 value = (Integer) ReflectionUtility.fetchMethod( component,
242 "getZIndex" ).invoke( component );
243 }
244 catch ( Throwable t )
245 {
246 logger.log( Level.SEVERE, "Error fetching z-index for FloatingPane", t );
247 }
248
249 if ( value > zIndex )
250 {
251 zIndex = value;
252 }
253 }
254 }
255
256 return ++zIndex;
257 }
258
259 /**
260 * Add the specified updater task to the task queue.
261 */
262 protected void addTask( Controller.Updater updater, Runnable runnable )
263 {
264 if ( taskQueue == null )
265 {
266 taskQueue = createTaskQueue();
267 }
268
269 updateTasks.put( updater, runnable );
270 updatePollingInterval();
271 }
272
273 /**
274 * Remove the specified updater task from the task queue.
275 */
276 protected void removeTask( Controller.Updater updater )
277 {
278 updateTasks.remove( updater );
279 updatePollingInterval();
280
281 if ( ( updateTasks.size() == 0 ) && ( taskQueue != null ) )
282 {
283 removeTaskQueue( taskQueue );
284 taskQueue = null;
285 }
286 }
287
288 /**
289 * Update {@link #pollingInterval} to the maximum value in the
290 * intervals for all the tasks that are queued.
291 */
292 protected void updatePollingInterval()
293 {
294 int[] intervals = new int[ updateTasks.size() ];
295 int count = 0;
296 for ( Controller.Updater updater : updateTasks.keySet() )
297 {
298 intervals[ count++ ] = updater.pollingInterval;
299 }
300
301 Arrays.sort( intervals );
302 pollingInterval =
303 ( intervals[ intervals.length - 1 ] < MINIMUM_INTERVAL ) ?
304 MINIMUM_INTERVAL : intervals[ intervals.length - 1 ];
305
306 logger.fine( "setting polling interval: " + pollingInterval );
307 ContainerContext containerContext =
308 (ContainerContext) getContextProperty(
309 ContainerContext.CONTEXT_PROPERTY_NAME );
310 containerContext.setTaskQueueCallbackInterval(
311 taskQueue, pollingInterval );
312 }
313
314 /**
315 * Over-ridden to enqueue each task in {@link #updateTasks}.
316 */
317 @Override
318 public boolean hasQueuedTasks()
319 {
320 for ( Map.Entry<Controller.Updater,Runnable> entry : updateTasks.entrySet() )
321 {
322 enqueueTask( entry.getKey(), entry.getValue() );
323 }
324
325 return super.hasQueuedTasks();
326 }
327
328 /**
329 * Return the <code>Locale</code>s specified by the client browser for the
330 * current session. The <code>getLocale</code> method inherited from
331 * <code>ApplicationInstance</code> returns only the locale of the server.
332 *
333 * @return The locales specified by the client browser.
334 */
335 public Locale[] getLocales()
336 {
337 ContainerContext containerContext =
338 (ContainerContext) getContextProperty(
339 ContainerContext.CONTEXT_PROPERTY_NAME );
340 ClientProperties clientProperties =
341 containerContext.getClientProperties();
342 return (Locale[]) clientProperties.get( ClientProperties.LOCALES );
343 }
344
345 /**
346 * Return the timezone of the client browser. Initialises {@link
347 * #timeZone} lazily.
348 *
349 * @return The timezone of the client browser.
350 */
351 public TimeZone getTimeZone()
352 {
353 if ( timeZone == null )
354 {
355 ContainerContext containerContext =
356 (ContainerContext) getContextProperty(
357 ContainerContext.CONTEXT_PROPERTY_NAME );
358 ClientProperties clientProperties =
359 containerContext.getClientProperties();
360
361 int offset = clientProperties.getInt( ClientProperties.UTC_OFFSET, 0 );
362
363 timeZone = TIME_ZONES.get( getOffset( offset * 60 * 1000 ) );
364 if ( timeZone == null )
365 {
366 timeZone =
367 TimeZone.getTimeZone( "GMT" + getOffset( offset * 60 * 1000 ) );
368 }
369 }
370
371 return timeZone;
372 }
373
374 /**
375 * Parse the specified offset from UTC in milliseconds and return it in
376 * <code>HHmm</code> format. This is used to lookup <code>TimeZone</code>
377 * instances.
378 *
379 * @param offset The offset from UTC in milliseconds.
380 * @return The string representation in proper format.
381 */
382 public static String getOffset( int offset )
383 {
384 String hours = String.valueOf( offset / ( 60 * 60 * 1000 ) );
385 String minutes = String.valueOf( ( offset / ( 60 * 1000 ) ) % 60 );
386
387 if ( hours.charAt( 0 ) == '-' && hours.length() == 2 )
388 {
389 hours = hours.substring( 0, 1 ) + "0" + hours.substring( 1 );
390 }
391 else if ( hours.length() == 1 )
392 {
393 hours = "0" + hours;
394 }
395
396 if ( minutes.length() == 1 )
397 {
398 minutes = "0" + minutes;
399 }
400
401 return String.valueOf( hours ) + minutes;
402 }
403
404 /**
405 * Return the view component that is the parent of the specified
406 * component.
407 *
408 * @param component The component whose parent view is to be returned.
409 * @return The parent view or <code>null</code> if no parent that is an
410 * instance of view exists.
411 */
412 public static View getParentView( Component component )
413 {
414 View view = null;
415 Component c = null;
416
417 while ( ( c = component.getParent() ) != null )
418 {
419 if ( c instanceof View )
420 {
421 view = (View) c;
422 break;
423 }
424 }
425
426 return view;
427 }
428
429 /**
430 * Return {@link #contentPane}, the fully qualified name of the
431 * <code>ContentPane</code> used in {@link #window}.
432 *
433 * @return The fully qualified name of the content pane.
434 */
435 public String getContentPane()
436 {
437 return contentPane;
438 }
439
440 /**
441 * Sets the values of {@link #contentPane} to the fully qualified name of
442 * the <code>ContentPane</code> class.
443 *
444 * @param contentPane The value to set.
445 */
446 public void setContentPane( String contentPane )
447 {
448 this.contentPane = contentPane;
449 }
450
451 /**
452 * Return {@link #styleSheet}.
453 *
454 * @return The fully qualified name being used.
455 */
456 public String getStyleSheet()
457 {
458 return styleSheet;
459 }
460
461 /**
462 * Sets the value of {@link #styleSheet}.
463 *
464 * @param styleSheet The fully qualified name to set.
465 */
466 public void setStyleSheet( String styleSheet )
467 {
468 this.styleSheet = styleSheet;
469 }
470
471 /**
472 * Returns {@link #user}.
473 *
474 * @return The value/reference of/to user.
475 */
476 public User getUser()
477 {
478 return user;
479 }
480
481 /**
482 * Set {@link #user}.
483 *
484 * @param user The value to set.
485 */
486 public void setUser( User user )
487 {
488 this.user = user;
489 }
490 }