/******************************************************************************* * GenPlay, Einstein Genome Analyzer * Copyright (C) 2009, 2014 Albert Einstein College of Medicine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * Authors: Julien Lajugie <julien.lajugie@einstein.yu.edu> * Nicolas Fourel <nicolas.fourel@einstein.yu.edu> * Eric Bouhassira <eric.bouhassira@einstein.yu.edu> * * Website: <http://genplay.einstein.yu.edu> ******************************************************************************/ package edu.yu.einstein.genplay.core.manager; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import edu.yu.einstein.genplay.core.manager.application.ConfigurationManager; import edu.yu.einstein.genplay.exception.ExceptionManager; /** * A generic class to provides tool to handle the Undo / Redo / Reset actions * @author Julien Lajugie * @version 0.1 * @param <T> type of the object to restore with undo / redo / reset */ public class URRManager<T extends Serializable> implements Serializable { private static final long serialVersionUID = -7259155655274729887L; // generated ID private static final int SAVED_FORMAT_VERSION_NUMBER = 0; // saved format version private int length; // number of action that can be undone / redone private T currentObject; // current object private T initialObjectSaver; // Initial object to restore with the reset action. Used only for the serialization. private transient ByteArrayOutputStream initialObject; // the initial object in it's compressed form. Transient because a BAOS can't be serialized private transient List<ByteArrayOutputStream> undoList; // a list of object to restore with the undo action in a compressed form. private transient List<ByteArrayOutputStream> redoList; // a list of object to restore with the redo action in a compressed form. private List<T> undoListSaver; // the list of undo in an uncompressed form. Used only for the serialization. private List<T> redoListSaver; // the list of redo in an uncompressed form. Used only for the serialization. /** * Creates an instance of {@link URRManager} * @param length * @param initialObject the initial state of the object to save */ public URRManager(int length, T initialObject) { this.length = length; this.currentObject = initialObject; this.undoList = new LinkedList<ByteArrayOutputStream>(); this.redoList = new LinkedList<ByteArrayOutputStream>(); } /** * Delete the initial object */ public void deactivateReset () { setInitialObject(null); } /** * @return a deep clone of the current object */ @SuppressWarnings("unchecked") public URRManager<T> deepClone() { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(this); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); return (URRManager<T>) ois.readObject(); } catch (Exception e) { ExceptionManager.getInstance().caughtException(e); return null; } } /** * @return true if the redo action is available. False otherwise */ public boolean isRedoable() { return (redoList != null) && (!redoList.isEmpty()); } /** * @return true if the reset action is available. False otherwise */ public boolean isResetable() { return initialObject != null; } /** * @return true if the undo action is available. False otherwise */ public boolean isUndoable() { return (undoList != null) && (!undoList.isEmpty()); } /** * Serializes and zips the undo and the redo lists * after the unserialization of an instance. * @param in {@link ObjectInputStream} * @throws IOException * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.readInt(); length = in.readInt(); currentObject = (T) in.readObject(); initialObjectSaver = (T) in.readObject(); undoListSaver = (List<T>) in.readObject(); redoListSaver = (List<T>) in.readObject(); undoList = new LinkedList<ByteArrayOutputStream>(); redoList = new LinkedList<ByteArrayOutputStream>(); if (initialObjectSaver != null) { setInitialObject(serializeAndZip(initialObjectSaver)); initialObjectSaver = null; } if (undoListSaver != null) { // if the undo saver list is longer than the authorized count of // undo // we remove the first elements of the undo saver while ((length - undoListSaver.size()) < 0) { undoListSaver.remove(0); } for (T currentUndo : undoListSaver) { undoList.add(serializeAndZip(currentUndo)); } undoListSaver = null; } if (redoListSaver != null) { // if the redo saver list is longer than the authorized count of // undo // we remove the first elements of the redo saver while ((length - redoListSaver.size()) < 0) { redoListSaver.remove(0); } for (T currentRedo : redoListSaver) { redoList.add(serializeAndZip(currentRedo)); } redoListSaver = null; } } /** * Restores the last undone object * @return the restored object * @throws IOException * @throws ClassNotFoundException */ public T redo() throws IOException, ClassNotFoundException { if (this.isRedoable()) { ByteArrayOutputStream oldBaos = serializeAndZip(currentObject); undoList.add(oldBaos); int lastIndex = redoList.size() - 1; ByteArrayOutputStream newBaos = redoList.get(lastIndex); if (initialObject == null) { setInitialObject(oldBaos); } else if (initialObject.equals(newBaos)) { setInitialObject(null); } currentObject = unzipAndUnserialize(newBaos); redoList.remove(lastIndex); newBaos.close(); oldBaos.close(); return currentObject; } else { return null; } } /** * Restores the original states of the objects * @return the original states * @throws IOException * @throws ClassNotFoundException */ public T reset() throws IOException, ClassNotFoundException { set(unzipAndUnserialize(initialObject)); setInitialObject(null); return currentObject; } /** * Serializes and then zips the input parameter * @param inputObject * @return a serialized and ziped version of the input parameter * @throws IOException */ private ByteArrayOutputStream serializeAndZip(T inputObject) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream gz = new GZIPOutputStream(baos); ObjectOutputStream oos = new ObjectOutputStream(gz); oos.writeObject(inputObject); oos.flush(); oos.close(); gz.flush(); gz.close(); return baos; } /** * Sets a new states. This operation can be undone * @param newObject * @throws IOException */ public void set(T newObject) throws IOException { // FIXME urrmanager.set should be in a separate thread because it freezes the interface after an action performed on a big track if (newObject != null) { ByteArrayOutputStream oldBaos = null; // if it's the first operation if ((initialObject == null) && ConfigurationManager.getInstance().isResetTrack()) { oldBaos = serializeAndZip(currentObject); setInitialObject(oldBaos); oldBaos.close(); } // if we accept the undo operation if (length > 0) { // if the undoBinLists is full (ie: more elements than undo count in the config manager) if (undoList.size() >= length) { undoList.remove(0); } if (oldBaos == null) { oldBaos = serializeAndZip(currentObject); } undoList.add(oldBaos); oldBaos.close(); } currentObject = newObject; redoList.clear(); } } /** * @param initialObject the initialObject to set */ private void setInitialObject(ByteArrayOutputStream initialObject) { if (initialObject == null) { this.initialObject = null; } else if (ConfigurationManager.getInstance().isResetTrack()) { this.initialObject = initialObject; } } /** * Sets the number of undo saved * @param length number of undo saved */ public void setLength(int length) { if (length < 0) { throw new InvalidParameterException("The undo count must be positive"); } this.length = length; while (undoList.size() > length) { undoList.remove(0); } if (undoList.size() == 0) { while (redoList.size() > length) { redoList.remove(0); } } } /** * Undone the last action * @return the restored object * @throws IOException * @throws ClassNotFoundException */ public T undo() throws IOException, ClassNotFoundException { if (this.isUndoable()) { ByteArrayOutputStream oldBaos = serializeAndZip(currentObject); redoList.add(oldBaos); // we unserialize the last BinList in of the undo lists and we // remove it from the undo list int lastIndex = undoList.size() - 1; ByteArrayOutputStream newBaos = undoList.get(lastIndex); if (initialObject == null) { setInitialObject(oldBaos); } else if (initialObject.equals(newBaos)) { setInitialObject(null); } currentObject = unzipAndUnserialize(newBaos); undoList.remove(lastIndex); oldBaos.close(); newBaos.close(); return currentObject; } else { return null; } } /** * Unzips and then unserializes a specified ByteArrayOutputStream * @param baos * @return an unziped and unserialized representation of the input parameter * @throws IOException * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") private T unzipAndUnserialize(ByteArrayOutputStream baos) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); GZIPInputStream gz = new GZIPInputStream(bais); ObjectInputStream ois = new ObjectInputStream(gz); T outputObject = (T) ois.readObject(); ois.close(); gz.close(); return outputObject; } /** * Unzips and unserializes the undo and the redo lists so they can * be serialized with the rest of the current instance and saved. * This is because ByteArrayOutputStream can't be serialized * @param out {@link ObjectOutputStream} * @throws IOException */ private void writeObject(ObjectOutputStream out) throws IOException { out.writeInt(SAVED_FORMAT_VERSION_NUMBER); out.writeInt(length); out.writeObject(currentObject); try { // unserialize the initial BinList if (this.isResetable()) { initialObjectSaver = unzipAndUnserialize(initialObject); } // unserialize the undo BinLists if (this.isUndoable()) { undoListSaver = new ArrayList<T>(); for (ByteArrayOutputStream currentUndo : undoList) { undoListSaver.add(unzipAndUnserialize(currentUndo)); } } // unserialize the redo BinLists if (this.isRedoable()) { redoListSaver = new ArrayList<T>(); for (ByteArrayOutputStream currentRedo : redoList) { redoListSaver.add(unzipAndUnserialize(currentRedo)); } } // write the savers out.writeObject(initialObjectSaver); out.writeObject(undoListSaver); out.writeObject(redoListSaver); // delete the savers initialObjectSaver = null; undoListSaver = null; redoListSaver = null; } catch (ClassNotFoundException e) { throw new IOException(); } } }