org.nuiton.jaxx.runtime.swing.session.SwingSession Maven / Gradle / Ivy
The newest version!
/*
* #%L
* JAXX :: Runtime
* %%
* Copyright (C) 2008 - 2024 Code Lutin, Ultreia.io
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nuiton.jaxx.runtime.swing.session;
import com.google.common.collect.Sets;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.swingx.JXTable;
import javax.swing.JFrame;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import java.awt.Component;
import java.awt.Container;
import java.awt.Rectangle;
import java.awt.Window;
import java.beans.DefaultPersistenceDelegate;
import java.beans.Encoder;
import java.beans.ExceptionListener;
import java.beans.Expression;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Use to store and restore position and size of application. Default supported widgets are:
*
* - java.awt.Window (and subclasses)
* - javax.swing.JTabbedPane (and subclasses)
* - javax.swing.JSplitPane (and subclasses)
* - org.jdesktop.swingx.JXTable (and subclasses)
* - javax.swing.JTable (and subclasses)
*
*
* usage:
*
* - create SwingSession object
* - add component that you want save
* - explicite call to save
*
* You can use same SwingSession for multiple window but in this case you must
* have setName for each window with different name, otherwize there are
* collision between window component and result is undetermisitic.
*
* This code is partialy inspired from http://kenai.com/projects/bsaf/pages/Home
* project. This project is under LGPL v2.1 license. We can't reuse directly this
* library because to many fields and methods are private and we can't implements
* it and modify some behavior.
*
* @author poussin
* @author kmorin
* @since 2.5.16
*/
public class SwingSession {
private static final Logger log = LogManager.getLogger(SwingSession.class);
protected File file;
protected final boolean autoSave;
protected final Set registeredComponent = Sets.newIdentityHashSet();
/**
* State object registered to get and set State.
* key: class of component managed by the state; value: the state
*/
protected final Map stateManager = new HashMap<>();
/**
* state of all component added with add method.
* key: path of compoenent; value: State
*/
protected Map states;
/**
* Create a new swing session with the given parameters.
*
* If it fails to read the given file, then it will delete it from the fs and starts with a new empty file.
*
* @return the new swing session
* @since 2.8.6
* @deprecated since 2.10, the default behaviour is now to safely load the incoming file
*/
@Deprecated
public static SwingSession newSession(File file, boolean autoSave) {
return newSession(file, autoSave, new HashMap<>());
}
/**
* Create a new swing session with the given parameters.
*
* If it fails to read the given file, then it will delete it from the fs and starts with a new empty file.
*
* @return the new swing session
* @since 2.8.6
* @deprecated since 2.10, the default behaviour is now to safely load the incoming file
*/
@Deprecated
public static SwingSession newSession(File file, boolean autoSave, Map additionalStates) {
return new SwingSession(file, autoSave, additionalStates);
}
public SwingSession(File file, boolean autoSave) {
this(file, autoSave, new HashMap<>());
}
public SwingSession(File file, boolean autoSave, Map additionalStates) {
this.file = file;
this.autoSave = autoSave;
stateManager.put(Window.class, new WindowState());
stateManager.put(JTable.class, new JTableState());
stateManager.put(JTabbedPane.class, new JTabbedPaneState());
stateManager.put(JSplitPane.class, new JSplitPaneState());
stateManager.put(JXTable.class, new JXTableSwingSessionState());
stateManager.putAll(additionalStates);
if (file != null && file.exists()) {
loadSafeStates();
} else {
states = new HashMap<>();
}
}
public File getFile() {
return new File(file.getAbsolutePath());
}
public void setFile(File file) {
this.file = file;
}
/**
* Loads safely the states from the {@link #file}.
*
* If could not read the internal file, then will try to delete it.
*
* @since 2.10
*/
public void loadSafeStates() {
try {
states = loadStates(file);
} catch (IOException e) {
// reset file
if (log.isErrorEnabled()) {
log.error("Could not read swing session file: " + file, e);
}
if (file.exists()) {
// try to delete it
try {
FileUtils.forceDelete(file);
} catch (IOException e1) {
throw new RuntimeException("Could not delete file: " + file, e1);
}
}
states = new HashMap<>();
}
if (states == null) {
states = new HashMap<>();
}
}
@Override
protected void finalize() throws Throwable {
save();
super.finalize();
}
/* If an exception occurs in the XMLEncoder/Decoder, we want
* to throw an IOException. The exceptionThrow listener method
* doesn't throw a checked exception so we just set a flag
* here and check it when the encode/decode operation finishes
*/
static private class AbortExceptionListener implements ExceptionListener {
public Exception exception = null;
@Override
public void exceptionThrown(Exception e) {
if (exception == null) {
exception = e;
}
}
}
/* There are some (old) Java classes that aren't proper beans. Rectangle
* is one of these. When running within the secure sandbox, writing a
* Rectangle with XMLEncoder causes a security exception because
* DefaultPersistenceDelegate calls Field.setAccessible(true) to gain
* access to private fields. This is a workaround for that problem.
* A bug has been filed, see JDK bug ID 4741757
*/
private static class RectanglePD extends DefaultPersistenceDelegate {
public RectanglePD() {
super(new String[]{"x", "y", "width", "height"});
}
@Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Rectangle oldR = (Rectangle) oldInstance;
Object[] constructorArgs = new Object[]{
oldR.x, oldR.y, oldR.width, oldR.height
};
return new Expression(oldInstance, oldInstance.getClass(), "new", constructorArgs);
}
}
public void save() throws IOException {
updateState();
AbortExceptionListener el = new AbortExceptionListener();
ByteArrayOutputStream bst = new ByteArrayOutputStream();
XMLEncoder e = null;
/* Buffer the XMLEncoder's output so that decoding errors don't
* cause us to trash the current version of the specified file.
*/
try {
e = new XMLEncoder(bst);
e.setPersistenceDelegate(Rectangle.class, new RectanglePD());
e.setExceptionListener(el);
e.writeObject(states);
} finally {
if (e != null) {
e.close();
}
}
if (el.exception != null) {
try {
throw el.exception;
} catch (Exception e1) {
if (e1 instanceof IOException) {
throw (IOException) e1;
} else {
throw new IOException(e1);
}
}
// log.warn("save failed \"" + file + "\"", el.exception);
} else {
OutputStream ost = null;
try {
ost = new FileOutputStream(file);
ost.write(bst.toByteArray());
ost.close();
} finally {
IOUtils.closeQuietly(ost);
}
}
}
/**
* Loads the states from the file
*/
public Map loadStates(File file) throws IOException {
Map result = null;
if (file.exists()) {
XMLDecoder d = null;
try {
InputStream ist = new FileInputStream(file);
d = new XMLDecoder(ist);
AbortExceptionListener eee = new AbortExceptionListener();
d.setExceptionListener(eee);
Object bean = d.readObject();
if (eee.exception != null) {
log.warn("load failed \"" + file + "\"", eee.exception);
throw eee.exception;
} else {
result = (Map) bean;
}
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
} else {
throw new IOException(e);
}
} finally {
if (d != null) {
d.close();
}
}
}
return result;
}
public void updateState() {
walkThrowComponent("", registeredComponent,
new SaveStateAction());
}
public void add(Component c) {
add(c, false);
}
public void add(final Component c, boolean replace) {
if (c == null) {
return;
}
final String cName = getComponentName(c);
Object existingComponent = CollectionUtils.find(registeredComponent,
o -> {
Component comp = o;
String compName = getComponentName(comp);
return c.getClass().equals(comp.getClass())
&& cName.equals(compName);
});
if (existingComponent != null) {
if (replace) {
if (log.isDebugEnabled()) {
log.debug("replacing the component fir path /" + cName);
}
remove((Component) existingComponent);
} else {
log.warn(String.format(
"Component already added %s(%s)", c.getClass(), c.getName()));
return;
}
}
registeredComponent.add(c);
walkThrowComponent("",
Collections.singleton(c),
new RestoreStateAction());
}
/**
* Remove component from component to save
*
* @param c
*/
public void remove(Component c) {
registeredComponent.remove(c);
}
protected String getComponentName(Component c) {
String name = c.getName();
if (name == null) {
int n = c.getParent().getComponentZOrder(c);
if (n >= 0) {
Class clazz = c.getClass();
name = clazz.getSimpleName();
if (name.length() == 0) {
name = "Anonymous" + clazz.getSuperclass().getSimpleName();
}
name = name + n;
} else {
// Implies that the component tree is changing
// while we're computing the path. Punt.
log.warn("Couldn't compute pathname for " + c);
}
}
return name;
}
public State getStateManager(Class clazz) {
State result = null;
while (result == null && clazz != null) {
result = stateManager.get(clazz);
clazz = clazz.getSuperclass();
}
return result;
}
public void addToStateManager(Class component, State state) {
stateManager.put(component, state);
}
public State getStates(String path) {
return states.get(path);
}
public void setStates(String path, State state) {
this.states.put(path, state);
}
protected void walkThrowComponent(
String path, Collection roots, Action action) {
for (Component root : roots) {
if (root != null) {
String pathname = path + "/" + getComponentName(root);
State state = getStateManager(root.getClass());
if (state != null) {
action.doAction(this, pathname, root);
}
if (root instanceof Container) {
Component[] children = ((Container) root).getComponents();
if ((children != null) && (children.length > 0)) {
walkThrowComponent(pathname, Arrays.asList(children), action);
}
}
if (root instanceof JFrame) {
Component[] children = ((JFrame) root).getContentPane().getComponents();
if ((children != null) && (children.length > 0)) {
walkThrowComponent(pathname, Arrays.asList(children), action);
}
}
}
}
}
public interface Action {
void doAction(SwingSession session, String path, Component c);
}
public static class SaveStateAction implements Action {
@Override
public void doAction(SwingSession session, String path, Component c) {
State manager = session.getStateManager(c.getClass());
State state = manager.getState(c);
session.setStates(path, state);
}
}
public static class RestoreStateAction implements Action {
@Override
public void doAction(SwingSession session, String path, Component c) {
State manager = session.getStateManager(c.getClass());
State state = session.getStates(path);
if (state != null) {
manager.setState(c, state);
}
}
}
}