
org.jdesktop.application.SessionStorage Maven / Gradle / Ivy
Show all versions of bsaf Show documentation
/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
package org.jdesktop.application;
import org.jdesktop.application.session.SplitPaneProperty;
import org.jdesktop.application.session.WindowProperty;
import org.jdesktop.application.session.TableProperty;
import org.jdesktop.application.session.TabbedPaneProperty;
import org.jdesktop.application.session.PropertySupport;
import java.applet.Applet;
import java.awt.Component;
import java.awt.Container;
import java.awt.Window;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.swing.*;
/**
* Support for storing GUI state that persists between Application sessions.
*
* This class simplifies the common task of saving a little bit of an
* application's GUI "session" state when the application shuts down,
* and then restoring that state when the application is restarted.
* Session state is stored on a per component basis, and only for
* components with a {@link java.awt.Component#getName name} and for
* which a {@code PropertySupport} object has been defined and registeres.
* SessionState Properties that preserve the {@code bounds} {@code Rectangle}
* for Windows, the {@code dividerLocation} for {@code JSliderPanes} and the
* {@code selectedIndex} for {@code JTabbedPanes} are defined by default. The
* {@code ApplicationContext} {@link
* ApplicationContext#getSessionStorage getSessionStorage} method
* provides a shared {@code SessionStorage} object.
*
* A typical Application saves session state in its
* {@link Application#shutdown shutdown()} method, and then restores
* session state in {@link Application#startup startup()}:
*
* public class MyApplication extends Application {
* @Override protected void shutdown() {
* getContext().getSessionStorage().save(mainFrame, "session.xml");
* }
* @Override protected void startup() {
* ApplicationContext appContext = getContext();
* appContext.setVendorId("Sun");
* appContext.setApplicationId("SessionStorage1");
* // ... create the GUI rooted by JFrame mainFrame
* appContext.getSessionStorage().restore(mainFrame, "session.xml");
* }
* // ...
* }
*
* In this example, the bounds of {@code mainFrame} as well the
* session state for any of its {@code JSliderPane} or {@code
* JTabbedPane} will be saved when the application shuts down, and
* restored when the applications starts up again. Note: error
* handling has been omitted from the example.
*
* Session state is stored locally, relative to the user's
* home directory, by the {@code LocalStorage}
* {@link LocalStorage#save save} and {@link LocalStorage#save load}
* methods. The {@code startup} method must set the
* {@code ApplicationContext} {@code vendorId} and {@code applicationId}
* properties to ensure that the correct
* {@link LocalStorage#getDirectory local directory} is selected on
* all platforms. For example, on Windows XP, the full pathname
* for filename {@code "session.xml"} is typically:
*
* ${userHome}\Application Data\${vendorId}\${applicationId}\session.xml
*
* Where the value of {@code ${userHome}} is the the value of
* the Java System property {@code "user.home"}. On Solaris or
* Linux the file is:
*
* ${userHome}/.${applicationId}/session.xml
*
* and on OSX:
*
* ${userHome}/Library/Application Support/${applicationId}/session.xml
*
*
* @see ApplicationContext#getSessionStorage
* @see LocalStorage
*/
public class SessionStorage {
private static Logger logger = Logger.getLogger(SessionStorage.class.getName());
private final Map propertyMap;
private final ApplicationContext context;
/**
* Constructs a SessionStorage object. The following {@link
* PropertySupport PropertySupport} objects are registered by default:
*
*
*
* Base Component Type
* PropertySupport
* PropertySupport Value
*
*
* Window
* WindowProperty
* WindowState
*
*
* JTabbedPane
* TabbedPaneProperty
* TabbedPaneState
*
*
* JSplitPane
* SplitPaneProperty
* SplitPaneState
*
*
* JTable
* TableProperty
* TableState
*
*
*
* Applications typically would not create a {@code SessionStorage}
* object directly, they'd use the shared ApplicationContext value:
*
* ApplicationContext ctx = Application.getInstance(MyApplication.class).getContext();
* SessionStorage ss = ctx.getSesssionStorage();
*
*
*
* @param context
* @see ApplicationContext#getSessionStorage
* @see #getProperty(Class)
* @see #getProperty(Component)
*/
protected SessionStorage(ApplicationContext context) {
if (context == null) {
throw new IllegalArgumentException("null context");
}
this.context = context;
propertyMap = new HashMap();
propertyMap.put(Window.class, new WindowProperty());
propertyMap.put(JTabbedPane.class, new TabbedPaneProperty());
propertyMap.put(JSplitPane.class, new SplitPaneProperty());
propertyMap.put(JTable.class, new TableProperty());
}
/**
* Returns {@link ApplicationContext} which was used during creation of this
* {@code SessionStorage} object.
* @return the application context for this session storage object
*/
protected final ApplicationContext getContext() {
return context;
}
private void checkSaveRestoreArgs(Component root, String fileName) {
if (root == null) {
throw new IllegalArgumentException("null root");
}
if (fileName == null) {
throw new IllegalArgumentException("null fileName");
}
}
/* At some point we may replace this with a more complex scheme.
*/
private String getComponentName(Component c) {
return c.getName();
}
/* Returns a string that uniquely identifies this component, or null
* if Component c doesn't have a name per getComponentName(). The
* pathname is basically the name of all of the components, starting
* with c, separated by "/". This path is the reverse of what's
* typical, the first path element is c's name, rather than the name
* of c's root Window or Applet. That way pathnames can be
* distinguished without comparing much of the string. The names
* of intermediate components *can* be null, we substitute
* "[type][z-order]" for the name. Here's an example:
*
* JFrame myFrame = new JFrame();
* JPanel p = new JPanel() {}; // anonymous JPanel subclass
* JButton myButton = new JButton();
* myButton.setName("myButton");
* p.add(myButton);
* myFrame.add(p);
*
* getComponentPathname(myButton) =>
* "myButton/AnonymousJPanel0/null.contentPane/null.layeredPane/JRootPane0/myFrame"
*
* Notes about name usage in AWT/Swing: JRootPane (inexplicably) assigns
* names to it's children (layeredPane, contentPane, glassPane);
* all AWT components lazily compute a name. If we hadn't assigned the
* JFrame a name, it's name would have been "frame0".
*/
private String getComponentPathname(Component c) {
String name = getComponentName(c);
if (name == null) {
return null;
}
StringBuilder path = new StringBuilder(name);
while ((c.getParent() != null) && !(c instanceof Window) && !(c instanceof Applet)) {
c = c.getParent();
name = getComponentName(c);
if (name == null) {
int n = c.getParent().getComponentZOrder(c);
if (n >= 0) {
Class cls = c.getClass();
name = cls.getSimpleName();
if (name.length() == 0) {
name = "Anonymous" + cls.getSuperclass().getSimpleName();
}
name = name + n;
} else {
// Implies that the component tree is changing
// while we're computing the path. Punt.
logger.warning("Couldn't compute pathname for " + c);
return null;
}
}
path.append("/").append(name);
}
return path.toString();
}
/* Recursively walk the component tree, breadth first, storing the
* state - PropertySupport.getSessionState() - of named components under
* their pathname (the key) in stateMap.
*
* Note: the breadth first tree-walking code here should remain
* structurally identical to restoreTree().
*/
private void saveTree(List roots, Map stateMap) {
List allChildren = new ArrayList();
for (Component root : roots) {
if (root != null) {
PropertySupport p = getProperty(root);
if (p != null) {
String pathname = getComponentPathname(root);
if (pathname != null) {
Object state = p.getSessionState(root);
if (state != null) {
stateMap.put(pathname, state);
}
}
}
}
if (root instanceof Container) {
Component[] children = ((Container) root).getComponents();
if ((children != null) && (children.length > 0)) {
Collections.addAll(allChildren, children);
}
}
}
if (allChildren.size() > 0) {
saveTree(allChildren, stateMap);
}
}
/**
* Saves the state of each named component in the specified hierarchy to
* a file using {@link LocalStorage#save LocalStorage.save(fileName)}.
* Each component is visited in breadth-first order: if a {@code PropertySupport}
* {@link #getProperty(Component) exists} for that component,
* and the component has a {@link java.awt.Component#getName name}, then
* its {@link PropertySupport#getSessionState state} is saved.
*
* Component names can be any string however they must be unique
* relative to the name's of the component's siblings. Most Swing
* components do not have a name by default, however there are
* some exceptions: JRootPane (inexplicably) assigns names to it's
* children (layeredPane, contentPane, glassPane); and all AWT
* components lazily compute a name, so JFrame, JDialog, and
* JWindow also have a name by default.
*
* The type of sessionState values (i.e. the type of values
* returned by {@code PropertySupport.getSessionState}) must be one those
* supported by {@link java.beans.XMLEncoder XMLEncoder} and
* {@link java.beans.XMLDecoder XMLDecoder}, for example beans
* (null constructor, read/write properties), primitives, and
* Collections. Java bean classes and their properties must be
* public. Typically beans defined for this purpose are little
* more than a handful of simple properties. The JDK 6
* @ConstructorProperties annotation can be used to eliminate
* the need for writing set methods in such beans, e.g.
*
* public class FooBar {
* private String foo, bar;
* // Defines the mapping from constructor params to properties
* @ConstructorProperties({"foo", "bar"})
* public FooBar(String foo, String bar) {
* this.foo = foo;
* this.bar = bar;
* }
* public String getFoo() { return foo; } // don't need setFoo
* public String getBar() { return bar; } // don't need setBar
* }
*
*
* @param root the root of the Component hierarchy to be saved.
* @param fileName the {@code LocalStorage} filename.
* @throws IOException
* @see #restore
* @see ApplicationContext#getLocalStorage
* @see LocalStorage#save
* @see #getProperty(Component)
*/
public void save(Component root, String fileName) throws IOException {
checkSaveRestoreArgs(root, fileName);
Map stateMap = new HashMap();
saveTree(Collections.singletonList(root), stateMap);
LocalStorage lst = getContext().getLocalStorage();
lst.save(stateMap, fileName);
}
/* Recursively walk the component tree, breadth first, restoring the
* state - PropertySupport.setSessionState() - of named components for which
* there's a non-null entry under the component's pathName in
* stateMap.
*
* Note: the breadth first tree-walking code here should remain
* structurally identical to saveTree().
*/
private void restoreTree(List roots, Map stateMap) {
List allChildren = new ArrayList();
for (Component root : roots) {
if (root != null) {
PropertySupport p = getProperty(root);
if (p != null) {
String pathname = getComponentPathname(root);
if (pathname != null) {
Object state = stateMap.get(pathname);
if (state != null) {
p.setSessionState(root, state);
} else {
logger.warning("No saved state for " + root);
}
}
}
}
if (root instanceof Container) {
Component[] children = ((Container) root).getComponents();
if ((children != null) && (children.length > 0)) {
Collections.addAll(allChildren, children);
}
}
}
if (allChildren.size() > 0) {
restoreTree(allChildren, stateMap);
}
}
/**
* Restores each named component in the specified hierarchy
* from the session state loaded from
* a file using {@link LocalStorage#save LocalStorage.load(fileName)}.
* Each component is visited in breadth-first order: if a
* {@link #getProperty(Component) PropertySupport} exists for that component,
* and the component has a {@link java.awt.Component#getName name}, then
* its state is {@link PropertySupport#setSessionState restored}.
*
* @param root the root of the Component hierarchy to be restored.
* @param fileName the {@code LocalStorage} filename.
* @throws IOException
* @see #save
* @see ApplicationContext#getLocalStorage
* @see LocalStorage#save
* @see #getProperty(Component)
*/
public void restore(Component root, String fileName) throws IOException {
checkSaveRestoreArgs(root, fileName);
LocalStorage lst = getContext().getLocalStorage();
Map stateMap = (Map) (lst.load(fileName));
if (stateMap != null) {
restoreTree(Collections.singletonList(root), stateMap);
}
}
private void checkClassArg(Class cls) {
if (cls == null) {
throw new IllegalArgumentException("null class");
}
}
/**
* Returns the {@code PropertySupport} object that was
* {@link #putProperty registered} for the specified class
* or a superclass. If no PropertySupport has been registered,
* return null. To lookup the session state {@code PropertySupport}
* for a {@code Component} use {@link #getProperty(Component)}.
*
*
* @exception IllegalArgumentException if {@code cls} is null
* @param cls the class to which the returned {@code PropertySupport} applies
* @return the {@code PropertySupport} registered with {@code putProperty} for
* the specified class or the first one registered for a superclass
* of {@code cls}.
* @see #getProperty(Component)
* @see #putProperty
* @see #save
* @see #restore
*/
public PropertySupport getProperty(Class cls) {
checkClassArg(cls);
while (cls != null) {
PropertySupport p = propertyMap.get(cls);
if (p != null) {
return p;
}
cls = cls.getSuperclass();
}
return null;
}
/**
* Register a {@code PropertySupport} for the specified class.
*
* One can clear the {@code PropertySupport} for a class by setting the entry to null:
*
* sessionStorage.putProperty(myClass.class, null);
*
*
* Register a custom {@code PropertySupport}:
*
* ApplicationContext ctx = Application.getInstance(MyApplication.class).getContext();
* SessionStorage ss = ctx.getSesssionStorage();
* ctx.putProperty(JTable.class, new ExtendedTableProperty());
*
*
* @exception IllegalArgumentException if {@code cls} is null.
* @param cls the class to which {@code propertySupport} applies.
* @param propertySupport the {@code PropertySupport} object to register or null.
* @see #getProperty(Component)
* @see #getProperty(Class)
* @see #save
* @see #restore
*/
public void putProperty(Class cls, PropertySupport propertySupport) {
checkClassArg(cls);
// Remove property support for the clazz in case property argument is null
if (propertySupport == null) {
propertyMap.remove(cls);
return;
}
propertyMap.put(cls, propertySupport);
}
/**
* If a {@code sessionState PropertySupport} object exists for the
* specified Component return it, otherwise return null. This method
* is used by the {@link #save save} and {@link #restore restore} methods
* to lookup the {@code sessionState PropertySupport} object for each component
* to whose session state is to be saved or restored.
*
* The {@code putProperty} method registers a PropertySupport object for
* a class. One can specify a PropertySupport object for a single Swing
* component by setting the component's client property, like this:
*
* myJComponent.putClientProperty(PropertySupport.class, myPropertySupport);
*
* One can also create components that implement the
* {@code PropertySupport} interface directly.
*
* @param component the component to retrive the {@code PropertySupport} from
* @return if {@code component} implements {@code PropertySupport}, then
* {@code component}, if {@code component} is a {@code JComponent} with a
* {@code PropertySupport} valued
* {@link javax.swing.JComponent#getClientProperty client property} under
* (client property key) {@code PropertySupport}, then
* return that, otherwise return the value of
* {@code getProperty(component.getClass())}.
*
* @exception IllegalArgumentException if {@code Component component} is null.
* @see javax.swing.JComponent#putClientProperty
* @see #getProperty(Class)
* @see #putProperty
* @see #save
* @see #restore
*/
public final PropertySupport getProperty(Component component) {
if (component == null) {
throw new IllegalArgumentException("null component");
}
if (component instanceof PropertySupport) {
return (PropertySupport) component;
} else {
PropertySupport p = null;
if (component instanceof JComponent) {
Object v = ((JComponent) component).getClientProperty(PropertySupport.class);
p = (v instanceof PropertySupport) ? (PropertySupport) v : null;
}
return (p != null) ? p : getProperty(component.getClass());
}
}
}