/* * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is * subject to license terms. */ package org.mypsycho.swing.app; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.lang.ref.Reference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import org.mypsycho.beans.converter.TypeConverter; import org.mypsycho.swing.app.beans.ApplicationAction; import org.mypsycho.swing.app.beans.TaskMonitor; import org.mypsycho.swing.app.beans.TextActions; import org.mypsycho.swing.app.os.Plateform; import org.mypsycho.swing.app.session.LocalStorage; import org.mypsycho.swing.app.session.SessionStorage; import org.mypsycho.swing.app.task.TaskService; import org.mypsycho.swing.app.utils.SwingHelper; import org.mypsycho.util.Files; /** * A singleton that manages shared objects, like actions, resources, and tasks, * for {@code Applications}. * <p> * {@link Application Applications} use {@code ApplicationContext}, * via {@link Application#getContext}, to access global values and services. * The majority of the Swing Application Framework API can be accessed through {@code * ApplicationContext}. * * @see Application * @author Hans Muller (Hans.Muller@Sun.COM) */ public class ApplicationContext extends SwingBean { private static final String NAME_TRIM_SUFFIX = "Application"; /** Prefix to identify environment in injected context */ public static final String ENV_PREFIX = "env:"; /** Prefix to identify plateform in injected context */ public static final String OS_PROP = ENV_PREFIX + "os"; /** Prefix to name plateform in injected context */ public static final String OS_DISPLAY_PROP = OS_PROP + ".common-name"; public static final String ARCHIVE_PROP = ENV_PREFIX + "AppLibFile"; public static final String LIBPATH_PROP = ENV_PREFIX + "AppLibPath"; public static final String PATH_PROP = OS_PROP + ".archive"; public static final String LOOKNFEEL_PROP = ENV_PREFIX + "lnf"; public static final String CLIENT_PROPERTY = "application.context"; public static final String RESOURCE_MARKER = "application.resourceManager"; /** Prefix for client property change */ public static final String CLIENT_PREFIX = "client@"; public static final String TASK_SERVICES_PROPERTY = "taskServices"; public static final String SESSION_STORAGE_PROPERTY = "sessionStorage"; public static final String RESOURCE_MANAGER_PROPERTY = "resourceManager"; public static final String ACTION_MANAGER_PROPERTY = "actionManager"; public static final String LOCAL_STORAGE_PROPERTY = "localStorage"; public static final String PLATEFORM_PROP = "plateform"; final private Application application; private volatile transient Map<Object, Object> clientProperties; private final List<TaskService> taskServices; private final List<TaskService> taskServicesReadOnly; private ResourceManager resourceManager; private LocalStorage localStorage; private SessionStorage sessionStorage; private ComponentManager componentManager; private final PropertyChangeListener localeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { updateLocaleSharedContext(); } }; private List<?> localeShareds = null; private String plateformStrategy = null; // the default one private Plateform plateform = null; private TaskMonitor taskMonitor = null; protected ApplicationContext(Application app) { application = app; resourceManager = new ResourceManager(this); localStorage = new LocalStorage(this); sessionStorage = new SessionStorage(this); taskServices = new CopyOnWriteArrayList<TaskService>(); taskServices.add(new TaskService("default") { @Override public void failed(Object source, Throwable cause) { getApplication().exceptionThrown(Level.SEVERE, source, "Failure in task " + this, cause); } }); taskServicesReadOnly = Collections.unmodifiableList(taskServices); componentManager = new ComponentManager(this); } void assertCompatible(String name, ApplicationContext context) throws IllegalArgumentException { if (context != this) { String msg = "Property " + name + " must be bound to this context"; throw new IllegalArgumentException(msg); } } private void updateLocaleSharedContext() { for (Object shared : localeShareds) { getResourceManager().inject(shared, getApplication().getLocale()); } } protected void init() { initPlateform(); initResourceManager(); initApplicationContent(); } protected void initPlateform() { /* Initialize the ApplicationContext application properties */ plateform = Plateform.identification.getInstance(plateformStrategy).getPlateform(); try { plateform.getHook().init(getApplication()); } catch (IllegalStateException e) { application.exceptionThrown(Level.SEVERE, plateform, "Plateform initialization failed", e); } } private void initResourceManager(String name, File f) { if (f != null) { String value; try { value = f.getCanonicalPath(); } catch (IOException e) { value = f.getAbsolutePath(); } initResourceManager(name, value); } } private void initResourceManager(String name, String value) { if (value != null) { resourceManager.addGlobal(name, value); } } protected void initResourceManager() { resourceManager.addGlobals(ENV_PREFIX, System.getenv()); resourceManager.addGlobals(ENV_PREFIX, System.getProperties()); initResourceManager(OS_PROP, plateform.getId()); initResourceManager(OS_DISPLAY_PROP, plateform.getDisplay()); Class<?> appClass = getApplication().getClass(); // Application location based on class location initResourceManager(ARCHIVE_PROP, Files.getLocationArchive(appClass)); initResourceManager(LIBPATH_PROP, Files.getLocation(appClass)); // Application name based on class name String simpleName = appClass.getSimpleName(); String packageName = "noPackage"; if (appClass.getPackage() != null) { packageName = appClass.getPackage().getName(); } // else no package : naughty boy !! resourceManager.addGlobal(ENV_PREFIX + "AppClassName", simpleName); if (NAME_TRIM_SUFFIX.equals(simpleName)) { int lastPart = packageName.lastIndexOf('.'); if (lastPart != -1) { simpleName = packageName.substring(lastPart + 1); packageName = packageName.substring(0, lastPart); } } else if (simpleName.endsWith(NAME_TRIM_SUFFIX)) { // Meaningless suffix int size = simpleName.length() - NAME_TRIM_SUFFIX.length(); simpleName = simpleName.substring(0, size); } resourceManager.addGlobal(ENV_PREFIX + "AppDefaultName", simpleName); resourceManager.addGlobal(ENV_PREFIX + "AppPackage", packageName); } List<?> createSharedLocaleContext() { return Collections.singletonList(new TextActions(this)); } protected void initApplicationContent() { localeShareds = createSharedLocaleContext(); updateLocaleSharedContext(); getApplication().addPropertyChangeListener(Locales.LOCALE_PROP, localeListener); getResourceManager().inject(getApplication(), getApplication().getLocale()); } /** * The {@code Application} singleton, or null if {@code launch} hasn't * been called yet. * * @return the launched Application singleton. * @see Application#launch */ public final Application getApplication() { return application; } /** * The application's {@code ResourceManager} provides * read-only cached access to resources in ResourceBundles via the * {@link ResourceMap ResourceMap} class. * * @return this application's ResourceManager. * @see #getResourceMap(Class, Class) */ public final ResourceManager getResourceManager() { return resourceManager; } @SuppressWarnings("unchecked") public final <T> T getResource(Class<T> type, String name) { TypeConverter converter = getResourceManager().getConverter(); String value = application.getProperty(name); if (value == null) { return null; } Object result = converter.convert(type, value, application); if (result instanceof Reference) { return ((Reference<? extends T>) result).get(); } return (T) result; } /** * Change this application's {@code ResourceManager}. An * {@code ApplicationContext} subclass that * wanted to fundamentally change the way {@code ResourceMaps} were * created and cached could replace this property in its constructor. * <p> * Throws an IllegalArgumentException if resourceManager is null. * * @param resourceManager the new value of the resourceManager property. * @see #getResourceMap(Class, Class) * @see #getResourceManager */ protected void setResourceManager(ResourceManager resourceManager) { SwingHelper.assertNotNull(RESOURCE_MANAGER_PROPERTY, resourceManager); assertCompatible(RESOURCE_MANAGER_PROPERTY, resourceManager.getContext()); Object oldValue = this.resourceManager; this.resourceManager = resourceManager; firePropertyChange(RESOURCE_MANAGER_PROPERTY, oldValue, this.resourceManager); } /** * The shared {@link LocalStorage LocalStorage} object. * * @return the shared {@link LocalStorage LocalStorage} object. */ public final LocalStorage getLocalStorage() { return localStorage; } /** * The shared {@link LocalStorage LocalStorage} object. * * @param localStorage the shared {@link LocalStorage LocalStorage} object. */ protected void setLocalStorage(LocalStorage localStorage) { SwingHelper.assertNotNull(LOCAL_STORAGE_PROPERTY, localStorage); assertCompatible(LOCAL_STORAGE_PROPERTY, localStorage.getContext()); Object oldValue = this.localStorage; this.localStorage = localStorage; firePropertyChange(LOCAL_STORAGE_PROPERTY, oldValue, this.localStorage); } /** * The shared {@link SessionStorage SessionStorage} object. * * @return the shared {@link SessionStorage SessionStorage} object. */ public final SessionStorage getSessionStorage() { return sessionStorage; } /** * The shared {@link SessionStorage SessionStorage} object. * * @param sessionStorage the shared {@link SessionStorage SessionStorage} object. */ protected void setSessionStorage(SessionStorage sessionStorage) { SwingHelper.assertNotNull(SESSION_STORAGE_PROPERTY, sessionStorage); assertCompatible(SESSION_STORAGE_PROPERTY, sessionStorage.getContext()); Object oldValue = this.sessionStorage; this.sessionStorage = sessionStorage; firePropertyChange(SESSION_STORAGE_PROPERTY, oldValue, this.sessionStorage); } /** * Returns an <code>ArrayTable</code> used for * key/value "client properties" for this component. If the <code>clientProperties</code> table * doesn't exist, an empty one * will be created. * * @return an ArrayTable * @see #putClientProperty * @see #getClientProperty */ private Map<Object, Object> getClientProperties() { if (clientProperties == null) { clientProperties = new HashMap<Object, Object>(); } return clientProperties; } /** * Returns the value of the property with the specified key. Only * properties added with <code>putClientProperty</code> will return * a non-<code>null</code> value. * * @param key the being queried * @return the value of this property or <code>null</code> * @see #putClientProperty */ public final Object getClientProperty(Object key) { if (clientProperties == null) { // no client props return null; } synchronized (clientProperties) { return clientProperties.get(key); } } /** * Adds an arbitrary key/value "client property" to this component. * <p> * The <code>get/putClientProperty</code> methods provide access to a small per-instance * hashtable. Callers can use get/putClientProperty to annotate components that were created by * another module. For example, a layout manager might store per child constraints this way. * For example: * * <pre> * componentA.putClientProperty("to the left of", componentB); * </pre> * * If value is <code>null</code> this method will remove the property. Changes to client * properties are reported with <code>PropertyChange</code> events. The name of the property * (for the sake of PropertyChange events) is <code>key.toString()</code>. * <p> * The <code>clientProperty</code> dictionary is not intended to support large scale extensions * to JComponent nor should be it considered an alternative to subclassing when designing a new * component. * * @param key the new client property key * @param value the new client property value; if <code>null</code> this method will remove the * property * @see #getClientProperty * @see #addPropertyChangeListener */ public final void putClientProperty(Object key, Object value) { if (value == null && clientProperties == null) { // Both the value and ArrayTable are null, implying we don't // have to do anything. return; } getClientProperties(); // lazy init Object oldValue; synchronized (clientProperties) { oldValue = clientProperties.get(key); if (value != null) { clientProperties.put(key, value); } else if (oldValue != null) { clientProperties.remove(key); } else { // old == new == null return; } } firePropertyChange(CLIENT_PREFIX + key, oldValue, value); } /** * Return a shared {@code Clipboard}. * * @return A shared {@code Clipboard}. */ public Clipboard getClipboard() { Clipboard clipboard = (Clipboard) getClientProperty("clipboard"); if (clipboard == null) { try { clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); } catch (SecurityException e) { clipboard = new Clipboard("sandbox"); } putClientProperty("clipboard", clipboard); } return clipboard; } private List<TaskService> copyTaskServices() { return new ArrayList<TaskService>(taskServices); } /** * Register a new TaskService with the application. The task service * then be retrieved by name via {@link ApplicationContext#getTaskService(String)}. * * @param taskService Task service to register */ public void addTaskService(TaskService taskService) { if (taskService == null) { throw new IllegalArgumentException("null taskService"); } List<TaskService> oldValue = null; List<TaskService> newValue = null; boolean changed = false; synchronized (taskServices) { if (!taskServices.contains(taskService)) { oldValue = copyTaskServices(); taskServices.add(taskService); newValue = copyTaskServices(); changed = true; } } if (changed) { firePropertyChange(TASK_SERVICES_PROPERTY, oldValue, newValue); } } /** * Unregister a previously registered TaskService. The task service * is not shut down. * * @param taskService TaskService to unregister */ public void removeTaskService(TaskService taskService) { if (taskService == null) { throw new IllegalArgumentException("null taskService"); } List<TaskService> oldValue = null, newValue = null; boolean changed = false; synchronized (taskServices) { if (taskServices.contains(taskService)) { oldValue = copyTaskServices(); taskServices.remove(taskService); newValue = copyTaskServices(); changed = true; } } if (changed) { firePropertyChange("taskServices", oldValue, newValue); } } /** * Look up a task service by name. * * @param name Name of the task service to retrieve. * @return Task service found, or null if no service of that name found */ public TaskService getTaskService(String name) { if (name == null) { throw new IllegalArgumentException("null name"); } for (TaskService taskService : taskServices) { if (name.equals(taskService.getName())) { return taskService; } } return null; } /** * Returns the default TaskService, i.e. the one named "default": * <code>return getTaskService("default")</code>. The * {@link ApplicationAction#actionPerformed ApplicationAction actionPerformed} * method executes background <code>Tasks</code> on the default * TaskService. Application's can launch Tasks in the same way, e.g. * <pre> * Application.getInstance().getContext().getTaskService().execute(myTask); * </pre> * * @return the default TaskService. * @see #getTaskService(String) * */ public final TaskService getTaskService() { return getTaskService("default"); } /** * Returns a read-only view of the complete list of TaskServices. * * @return a list of all of the TaskServices. * @see #addTaskService * @see #removeTaskService */ public List<TaskService> getTaskServices() { return taskServicesReadOnly; } /** * Returns a shared TaskMonitor object. Most applications only * need one TaskMonitor for the sake of status bars and other status * indicators. * * @return the shared TaskMonitor object. */ public final TaskMonitor getTaskMonitor() { if (taskMonitor == null) { taskMonitor = new TaskMonitor(this); } return taskMonitor; } public void setPlateformStrategy(String plateformStrategy) { if (getApplication().getState() != null) { throw new IllegalStateException( "Plateform strategy cannot be modified if the application is launched"); } this.plateformStrategy = plateformStrategy; } public final Plateform getPlateform() { return plateform; } /** * Returns the componentManager. * * @return the componentManager */ public ComponentManager getComponentManager() { return componentManager; } }