All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy