001: /* *************************************************************************
002:
003: Millstone(TM)
004: Open Sourced User Interface Library for
005: Internet Development with Java
006:
007: Millstone is a registered trademark of IT Mill Ltd
008: Copyright (C) 2000,2001,2002 IT Mill Ltd
009:
010: *************************************************************************
011:
012: This library is free software; you can redistribute it and/or
013: modify it under the terms of the GNU Lesser General Public
014: License as published by the Free Software Foundation; either
015: version 2.1 of the License, or (at your option) any later version.
016:
017: This library is distributed in the hope that it will be useful,
018: but WITHOUT ANY WARRANTY; without even the implied warranty of
019: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
020: Lesser General Public License for more details.
021:
022: You should have received a copy of the GNU Lesser General Public
023: License along with this library; if not, write to the Free Software
024: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
025:
026: *************************************************************************
027:
028: For more information, contact:
029:
030: IT Mill Ltd phone: +358 2 4802 7180
031: Ruukinkatu 2-4 fax: +358 2 4802 7181
032: 20540, Turku email: info@itmill.com
033: Finland company www: www.itmill.com
034:
035: Primary source for MillStone information and releases: www.millstone.org
036:
037: ********************************************************************** */
038:
039: package org.millstone.base;
040:
041: import org.millstone.base.service.ApplicationContext;
042: import org.millstone.base.terminal.*;
043: import org.millstone.base.terminal.ApplicationResource;
044: import org.millstone.base.terminal.DownloadStream;
045: import org.millstone.base.terminal.Terminal;
046: import org.millstone.base.terminal.URIHandler;
047: import org.millstone.base.ui.AbstractComponent;
048: import org.millstone.base.ui.Window;
049: import java.util.Collection;
050: import java.util.Collections;
051: import java.util.Enumeration;
052: import java.util.EventListener;
053: import java.util.EventObject;
054: import java.util.Hashtable;
055: import java.util.Iterator;
056: import java.util.LinkedList;
057: import java.util.Locale;
058: import java.util.Properties;
059: import java.util.Random;
060: import java.net.MalformedURLException;
061: import java.net.URL;
062:
063: /** <p>Abstract base class required for all MillStone applications. This
064: * class provides all the basic services required by the MillStone
065: * framework. These services allow external discovery and manipulation of
066: * the user, {@link org.millstone.base.ui.Window windows} and
067: * themes, and starting and stopping the application.</p>
068: *
069: * <p>As mentioned, all MillStone applications must inherit this class.
070: * However, this is almost all of what one needs to do to create a fully
071: * functional MillStone application. The only thing a class inheriting the
072: * <code>Application</code> needs to do is implement the <code>init()</code>
073: * where it creates the windows it needs to perform its function. Note that
074: * all MillStone applications must have at least one window: the main
075: * window. The first unnamed window constructed by an application
076: * automatically becomes the main window which behaves just like other
077: * windows with one exception: when accessing windows using URLs the main
078: * window corresponds to the application URL whereas other windows
079: * correspond to a URL gotten by catenating the window's name to the
080: * application URL.</p>
081: *
082: * <p>See the class <code>org.millstone.examples.HelloWorld</code> for
083: * a simple example of a fully working MillStone Application.</p>
084: *
085: * <p><strong>Window access.</strong> <code>Application</code> provides
086: * methods to list, add and remove the windows it contains.</p>
087: *
088: * <p><strong>Execution control.</strong> This class includes method to
089: * start and finish the execution of the application. Being finished means
090: * basically that no windows will be available from the application
091: * anymore.</p>
092: *
093: * <p><strong>Theme selection.</strong> The MillStone theme selection
094: * process allows a theme to be specified at three different levels. When
095: * a window's theme needs to be found out, the window itself is queried
096: * for a preferred theme. If the window does not prefer a specific theme,
097: * the application containing the window is queried. If neither the
098: * application prefers a theme, the default theme for the
099: * {@link org.millstone.base.terminal.Terminal terminal} is used. The
100: * terminal always defines a default theme.</p>
101: *
102: * @author IT Mill Ltd.
103: * @version 3.1.1
104: * @since 3.0
105: */
106: public abstract class Application implements URIHandler,
107: Terminal.ErrorListener {
108:
109: /** Random window name generator */
110: private static Random nameGenerator = new Random();
111:
112: /** Application context the application is running in */
113: private ApplicationContext context;
114:
115: /** The current user or <code>null</code> if no user has logged in. */
116: private Object user;
117:
118: /** Mapping from window name to window instance */
119: private Hashtable windows = new Hashtable();
120:
121: /** Main window of the application. */
122: private Window mainWindow = null;
123:
124: /** The application's URL. */
125: private URL applicationUrl;
126:
127: /** Name of the theme currently used by the application. */
128: private String theme = null;
129:
130: /** Application status */
131: private boolean applicationIsRunning = false;
132:
133: /** Application properties */
134: private Properties properties;
135:
136: /** Default locale of the application. */
137: private Locale locale;
138:
139: /** List of listeners listening user changes */
140: private LinkedList userChangeListeners = null;
141:
142: /** Window attach listeners */
143: private LinkedList windowAttachListeners = null;
144:
145: /** Window detach listeners */
146: private LinkedList windowDetachListeners = null;
147:
148: /** Application error listeners */
149: private LinkedList errorListeners = null;
150:
151: /** Application resource mapping: key <-> resource */
152: private Hashtable resourceKeyMap = new Hashtable();
153: private Hashtable keyResourceMap = new Hashtable();
154: private long lastResourceKeyNumber = 0;
155:
156: /** URL the user is redirected to on application close or
157: * null if application is just closed */
158: private String logoutURL = null;
159:
160: /** Gets a window by name. Returns <code>null</code>
161: * if the application is not running or it does not contain a window
162: * corresponding to <code>name</code>.
163: *
164: * @param name The name of the window.
165: * @return The window associated with the given URI or <code>null</code>
166: */
167: public Window getWindow(String name) {
168:
169: // For closed app, do not give any windows
170: if (!isRunning())
171: return null;
172:
173: // Get the window by name
174: Window window = (Window) windows.get(name);
175:
176: return window;
177: }
178:
179: /** Adds a new window to the application.
180: *
181: * <p>This implicitly invokes the
182: * {@link org.millstone.base.ui.Window#setApplication(Application)}
183: * method. </p>
184: *
185: * @param window the new <code>Window</code> to add
186: * @throws IllegalArgumentException if a window with the same name
187: * as the new window already exists in the application
188: * @throws NullPointerException if the given <code>Window</code> or
189: * its name is <code>null</code>
190: */
191: public void addWindow(Window window)
192: throws IllegalArgumentException, NullPointerException {
193:
194: // Nulls can not be added to application
195: if (window == null)
196: return;
197:
198: // Get the naming proposal from window
199: String name = window.getName();
200:
201: // Check that the application does not already contain
202: // window having the same name
203: if (name != null && windows.containsKey(name)) {
204:
205: // If the window is already added
206: if (window == windows.get(name))
207: return;
208:
209: // Otherwise complain
210: throw new IllegalArgumentException("Window with name '"
211: + window.getName()
212: + "' is already present in the application");
213: }
214:
215: // If the name of the window is null, the window is automatically named
216: if (name == null) {
217: boolean accepted = false;
218: while (!accepted) {
219:
220: // Try another name
221: name = String
222: .valueOf(Math.abs(nameGenerator.nextInt()));
223: if (!windows.containsKey(name))
224: accepted = true;
225: }
226: window.setName(name);
227: }
228:
229: // Add the window to application
230: windows.put(name, window);
231: window.setApplication(this );
232:
233: // Fire window attach event
234: if (windowAttachListeners != null) {
235: Object[] listeners = windowAttachListeners.toArray();
236: WindowAttachEvent event = new WindowAttachEvent(window);
237: for (int i = 0; i < listeners.length; i++) {
238: ((WindowAttachListener) listeners[i])
239: .windowAttached(event);
240: }
241: }
242:
243: // If no main window is set, declare the window to be main window
244: if (getMainWindow() == null)
245: setMainWindow(window);
246: }
247:
248: /** Removes the specified window from the application.
249: *
250: * @param window The window to be removed
251: */
252: public void removeWindow(Window window) {
253: if (window != null && windows.contains(window)) {
254:
255: // Remove window from application
256: windows.remove(window.getName());
257:
258: // If the window was main window, clear it
259: if (getMainWindow() == window)
260: setMainWindow(null);
261:
262: // Remove application from window
263: if (window.getApplication() == this )
264: window.setApplication(null);
265:
266: // Fire window detach event
267: if (windowDetachListeners != null) {
268: Object[] listeners = windowDetachListeners.toArray();
269: WindowDetachEvent event = new WindowDetachEvent(window);
270: for (int i = 0; i < listeners.length; i++) {
271: ((WindowDetachListener) listeners[i])
272: .windowDetached(event);
273: }
274: }
275: }
276: }
277:
278: /** Gets the user of the application.
279: *
280: * @return User of the application.
281: */
282: public Object getUser() {
283: return user;
284: }
285:
286: /** Sets the user of the application instance. An application instance
287: * may have a user associated to it. This can be set in login procedure
288: * or application initialization. A component performing the user login
289: * procedure can assign the user property of the application and make
290: * the user object available to other components of the application.
291: *
292: * @param user the new user.
293: */
294: public void setUser(Object user) {
295: Object prevUser = this .user;
296: if (user != prevUser
297: && (user == null || !user.equals(prevUser))) {
298: this .user = user;
299: if (userChangeListeners != null) {
300: Object[] listeners = userChangeListeners.toArray();
301: UserChangeEvent event = new UserChangeEvent(this , user,
302: prevUser);
303: for (int i = 0; i < listeners.length; i++) {
304: ((UserChangeListener) listeners[i])
305: .applicationUserChanged(event);
306: }
307: }
308: }
309: }
310:
311: /** Gets the URL of the application.
312: *
313: * @return the application's URL.
314: */
315: public URL getURL() {
316: return applicationUrl;
317: }
318:
319: /** Ends the Application. In effect this will cause the application stop
320: * returning any windows when asked.
321: */
322: public void close() {
323: applicationIsRunning = false;
324: }
325:
326: /** Starts the application on the given URL. After this call the
327: * application corresponds to the given URL and it will return windows
328: * when asked for them.
329: *
330: * @param applicationUrl The URL the application should respond to
331: * @param applicationProperties Application properties as specified by the adapter.
332: * @param context The context application will be running in
333: *
334: */
335: public void start(URL applicationUrl,
336: Properties applicationProperties, ApplicationContext context) {
337: this .applicationUrl = applicationUrl;
338: this .properties = applicationProperties;
339: this .context = context;
340: init();
341: applicationIsRunning = true;
342: }
343:
344: /** Tests if the application is running or if it has it been finished.
345: *
346: * @return <code>true</code> if the application is running,
347: * <code>false</code> if not
348: */
349: public boolean isRunning() {
350: return applicationIsRunning;
351: }
352:
353: /** Gets the set of windows contained by the application.
354: *
355: * @return Unmodifiable collection of windows
356: */
357: public Collection getWindows() {
358: return Collections.unmodifiableCollection(windows.values());
359: }
360:
361: /** Main initializer of the application. This method is called by the
362: * framework when the application is started, and it should perform
363: * whatever initialization operations the application needs, such
364: * as creating windows and adding components to them.
365: */
366: public abstract void init();
367:
368: /** Gets the application's theme. The application's theme is the default
369: * theme used by all the windows in it that do not explicitly specify a
370: * theme. If the application theme is not explicitly set, the
371: * <code>null</code> is returned.
372: *
373: * @return the name of the application's theme
374: */
375: public String getTheme() {
376: return theme;
377: }
378:
379: /** Sets the application's theme. Note that this theme can be overridden
380: * by the windows. <code>null</code> implies the default terminal theme.
381: *
382: * @param theme The new theme for this application
383: */
384: public void setTheme(String theme) {
385:
386: // Collect list of windows not having the current or future theme
387: LinkedList toBeUpdated = new LinkedList();
388: String myTheme = this .getTheme();
389: for (Iterator i = getWindows().iterator(); i.hasNext();) {
390: Window w = (Window) i.next();
391: String windowTheme = w.getTheme();
392: if ((windowTheme == null)
393: || (!theme.equals(windowTheme) && windowTheme
394: .equals(myTheme))) {
395: toBeUpdated.add(w);
396: }
397: }
398:
399: // Update theme
400: this .theme = theme;
401:
402: // Ask windows to update themselves
403: for (Iterator i = getWindows().iterator(); i.hasNext();)
404: ((Window) i.next()).requestRepaint();
405: }
406:
407: /** Returns the mainWindow of the application.
408: * @return Window
409: */
410: public Window getMainWindow() {
411: return mainWindow;
412: }
413:
414: /** Sets the mainWindow. If the main window is not explicitly set, the
415: * main window defaults to first created window. Setting window as
416: * a main window of this application also adds the window to this application.
417: * @param mainWindow The mainWindow to set
418: */
419: public void setMainWindow(Window mainWindow) {
420:
421: addWindow(mainWindow);
422: this .mainWindow = mainWindow;
423: }
424:
425: /** Returns an enumeration of all the names in this application.
426: * @return an enumeration of all the keys in this property list, including the keys in
427: * the default property list.
428: *
429: */
430: public Enumeration getPropertyNames() {
431: return this .properties.propertyNames();
432: }
433:
434: /** Searches for the property with the specified name in this application.
435: * The method returns null if the property is not found.
436: *
437: * @param name The name of the property.
438: * @return The value in this property list with the specified key value.
439: */
440: public String getProperty(String name) {
441: return this .properties.getProperty(name);
442: }
443:
444: /** Add new resource to the application. The resource can be
445: * accessed by the user of the application. */
446: public void addResource(ApplicationResource resource) {
447:
448: // Check if the resource is already mapped
449: if (resourceKeyMap.containsKey(resource))
450: return;
451:
452: // Generate key
453: String key = String.valueOf(++lastResourceKeyNumber);
454:
455: // Add the resource to mappings
456: resourceKeyMap.put(resource, key);
457: keyResourceMap.put(key, resource);
458: }
459:
460: /** Remove resource from the application. */
461: public void removeResource(ApplicationResource resource) {
462: Object key = resourceKeyMap.get(resource);
463: if (key != null) {
464: resourceKeyMap.remove(resource);
465: keyResourceMap.remove(key);
466: }
467: }
468:
469: /** Get relative uri of the resource */
470: public String getRelativeLocation(ApplicationResource resource) {
471:
472: // Get the key
473: String key = (String) resourceKeyMap.get(resource);
474:
475: // If the resource is not registered, return null
476: if (key == null)
477: return null;
478:
479: String filename = resource.getFilename();
480: if (filename == null)
481: return "APP/" + key + "/";
482: else
483: return "APP/" + key + "/" + filename;
484: }
485:
486: /* @see org.millstone.base.terminal.URIHandler#handleURI(URL, String)
487: */
488: public DownloadStream handleURI(URL context, String relativeUri) { // If the relative uri is null, we are ready
489: if (relativeUri == null)
490: return null;
491:
492: // Resolve prefix
493: String prefix = relativeUri;
494: int index = relativeUri.indexOf('/');
495: if (index >= 0)
496: prefix = relativeUri.substring(0, index);
497:
498: // Handle resource requests
499: if (prefix.equals("APP")) {
500:
501: // Handle resource request
502: int next = relativeUri.indexOf('/', index + 1);
503: if (next < 0)
504: return null;
505: String key = relativeUri.substring(index + 1, next);
506: ApplicationResource resource = (ApplicationResource) keyResourceMap
507: .get(key);
508: if (resource != null)
509: return resource.getStream();
510:
511: // Resource requests override uri handling
512: return null;
513: }
514:
515: // If the uri is in some window, handle the window uri
516: Window window = getWindow(prefix);
517: if (window != null) {
518: URL windowContext;
519: try {
520: windowContext = new URL(context, prefix + "/");
521: String windowUri = relativeUri.length() > prefix
522: .length() + 1 ? relativeUri.substring(prefix
523: .length() + 1) : "";
524: return window.handleURI(windowContext, windowUri);
525: } catch (MalformedURLException e) {
526: return null;
527: }
528: }
529:
530: // If the uri was not pointing to a window, handle the
531: // uri in main window
532: window = getMainWindow();
533: if (window != null)
534: return window.handleURI(context, relativeUri);
535:
536: return null;
537: }
538:
539: /** Get thed default locale for this application */
540: public Locale getLocale() {
541: if (this .locale != null)
542: return this .locale;
543: return Locale.getDefault();
544: }
545:
546: /** Set the default locale for this application */
547: public void setLocale(Locale locale) {
548: this .locale = locale;
549: }
550:
551: /** Application user change event sent when the setUser is called to
552: * change the current user of the application.
553: * @version 3.1.1
554: * @since 3.0
555: */
556: public class UserChangeEvent extends java.util.EventObject {
557:
558: /**
559: * Serial generated by eclipse.
560: */
561: private static final long serialVersionUID = 3544951069307188281L;
562:
563: /** New user of the application */
564: private Object newUser;
565:
566: /** Previous user of the application */
567: private Object prevUser;
568:
569: /** Contructor for user change event */
570: public UserChangeEvent(Application source, Object newUser,
571: Object prevUser) {
572: super (source);
573: this .newUser = newUser;
574: this .prevUser = prevUser;
575: }
576:
577: /** Get the new user of the application */
578: public Object getNewUser() {
579: return newUser;
580: }
581:
582: /** Get the previous user of the application */
583: public Object getPreviousUser() {
584: return prevUser;
585: }
586:
587: /** Get the application where the user change occurred */
588: public Application getApplication() {
589: return (Application) getSource();
590: }
591: }
592:
593: /** Public interface for listening application user changes
594: * @version 3.1.1
595: * @since 3.0
596: */
597: public interface UserChangeListener extends EventListener {
598:
599: /** Invoked when the application user has changed */
600: public void applicationUserChanged(
601: Application.UserChangeEvent event);
602: }
603:
604: /** Add user change listener */
605: public void addListener(UserChangeListener listener) {
606: if (userChangeListeners == null)
607: userChangeListeners = new LinkedList();
608: userChangeListeners.add(listener);
609: }
610:
611: /** Remove user change listener */
612: public void removeListener(UserChangeListener listener) {
613: if (userChangeListeners == null)
614: return;
615: userChangeListeners.remove(listener);
616: if (userChangeListeners.isEmpty())
617: userChangeListeners = null;
618: }
619:
620: /** Window detach event */
621: public class WindowDetachEvent extends EventObject {
622:
623: /**
624: * Serial generated by eclipse.
625: */
626: private static final long serialVersionUID = 3544669568644691769L;
627: private Window window;
628:
629: /** Create event.
630: * @param window Detached window.
631: */
632: public WindowDetachEvent(Window window) {
633: super (Application.this );
634: this .window = window;
635: }
636:
637: /** Get the detached window */
638: public Window getWindow() {
639: return window;
640: }
641:
642: /** Get the application from which the window was detached */
643: public Application getApplication() {
644: return (Application) getSource();
645: }
646: }
647:
648: /** Window attach event */
649: public class WindowAttachEvent extends EventObject {
650:
651: /**
652: * Serial generated by eclipse.
653: */
654: private static final long serialVersionUID = 3977578104367822392L;
655: private Window window;
656:
657: /** Create event.
658: * @param window Attached window.
659: */
660: public WindowAttachEvent(Window window) {
661: super (Application.this );
662: this .window = window;
663: }
664:
665: /** Get the attached window */
666: public Window getWindow() {
667: return window;
668: }
669:
670: /** Get the application to which the window was attached */
671: public Application getApplication() {
672: return (Application) getSource();
673: }
674: }
675:
676: /** Window attach listener interface */
677: public interface WindowAttachListener {
678:
679: /** Window attached */
680: public void windowAttached(WindowAttachEvent event);
681: }
682:
683: /** Window detach listener interface */
684: public interface WindowDetachListener {
685:
686: /** Window attached */
687: public void windowDetached(WindowDetachEvent event);
688: }
689:
690: /** Add window attach listener */
691: public void addListener(WindowAttachListener listener) {
692: if (windowAttachListeners == null)
693: windowAttachListeners = new LinkedList();
694: windowAttachListeners.add(listener);
695: }
696:
697: /** Add window detach listener */
698: public void addListener(WindowDetachListener listener) {
699: if (windowDetachListeners == null)
700: windowDetachListeners = new LinkedList();
701: windowDetachListeners.add(listener);
702: }
703:
704: /** Remove window attach listener */
705: public void removeListener(WindowAttachListener listener) {
706: if (windowAttachListeners != null) {
707: windowAttachListeners.remove(listener);
708: if (windowAttachListeners.isEmpty())
709: windowAttachListeners = null;
710: }
711: }
712:
713: /** Remove window detach listener */
714: public void removeListener(WindowDetachListener listener) {
715: if (windowDetachListeners != null) {
716: windowDetachListeners.remove(listener);
717: if (windowDetachListeners.isEmpty())
718: windowDetachListeners = null;
719: }
720: }
721:
722: /** Returns the URL user is redirected to on application close.
723: * If the URL is null, the application is closed normally as
724: * defined by the application running environment: Desctop application
725: * just closes the application window and web-application redirects
726: * the browser to application main URL.
727: *
728: * @return URL
729: */
730: public String getLogoutURL() {
731: return logoutURL;
732: }
733:
734: /** Sets the URL user is redirected to on application close.
735: * If the URL is null, the application is closed normally as
736: * defined by the application running environment: Desctop application
737: * just closes the application window and web-application redirects
738: * the browser to application main URL.
739: *
740: * @param logoutURL The logoutURL to set
741: */
742: public void setLogoutURL(String logoutURL) {
743: this .logoutURL = logoutURL;
744: }
745:
746: /**
747: * @see org.millstone.base.terminal.Terminal.ErrorListener#terminalError(org.millstone.base.terminal.Terminal.ErrorEvent)
748: */
749: public void terminalError(Terminal.ErrorEvent event) {
750:
751: // Find the original source of the error/exception
752: Object owner = null;
753: if (event instanceof VariableOwner.ErrorEvent) {
754: owner = ((VariableOwner.ErrorEvent) event)
755: .getVariableOwner();
756: } else if (event instanceof URIHandler.ErrorEvent) {
757: owner = ((URIHandler.ErrorEvent) event).getURIHandler();
758: } else if (event instanceof ParameterHandler.ErrorEvent) {
759: owner = ((ParameterHandler.ErrorEvent) event)
760: .getParameterHandler();
761: }
762:
763: // Show the error in AbstractComponent
764: if (owner instanceof AbstractComponent) {
765: Throwable e = event.getThrowable();
766: if (e instanceof ErrorMessage)
767: ((AbstractComponent) owner)
768: .setComponentError((ErrorMessage) e);
769: else
770: ((AbstractComponent) owner)
771: .setComponentError(new SystemError(e));
772: }
773: }
774:
775: /** Get application context.
776: *
777: * The application context is the environment where the application
778: * is running in.
779: */
780: public ApplicationContext getContext() {
781: return context;
782: }
783:
784: }
|