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    }