/** * Copyright (C) 2013-2014 Olaf Lessenich * Copyright (C) 2014-2015 University of Passau, Germany * * This library 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 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA * * Contributors: * Olaf Lessenich <lessenic@fim.uni-passau.de> * Georg Seibt <seibt@fim.uni-passau.de> */ package de.fosd.jdime.gui; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; 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.ArrayList; import java.util.Collection; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; /** * A history of GUI <code>State</code>s. The <code>History</code> stores an index into the list of stored * <code>State</code>s it represents. One additional <code>State</code> is stored representing the state of the GUI * before a state from the <code>History</code> was loaded. This <code>State</code> can be thought of as being at the * index returned by {@link #getSize()}. The <code>applyX</code> methods advance/regress the index by one and apply * the <code>State</code> at the new index to the <code>GUI</code>. */ public class History { private static final Logger LOG = Logger.getLogger(History.class.getCanonicalName()); private static XStream serializer; static { serializer = new XStream(); serializer.registerConverter(new Converter() { @Override public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { context.convertAnother(((History) source).history.get()); } @Override @SuppressWarnings("unchecked") public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { return new History((ObservableList<State>) context.convertAnother(reader, ObservableList.class)); } @Override public boolean canConvert(@SuppressWarnings("rawtypes") Class type) { return type.equals(History.class); } }); serializer.registerConverter(new Converter() { private Converter listConverter = serializer.getConverterLookup().lookupConverterForType(ArrayList.class); @Override public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { listConverter.marshal(new ArrayList<>((ObservableList<?>) source), writer, context); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { return FXCollections.observableArrayList((Collection<?>) context.convertAnother(reader, ArrayList.class)); } @Override public boolean canConvert(@SuppressWarnings("rawtypes") Class type) { return ObservableList.class.isAssignableFrom(type); } }); serializer.alias("history", History.class); serializer.omitField(State.class, "treeViewTabs"); serializer.alias("state", State.class); } private IntegerProperty index; private SimpleListProperty<State> history; private State inProgress; private ReadOnlyBooleanProperty hasPrevious; private ReadOnlyBooleanProperty hasNext; /** * Constructs a new <code>History</code>. */ public History() { this(FXCollections.observableArrayList()); } /** * Private constructor used for deserialization purposes. * * @param history * the <code>ObservableList</code> to use as the history */ private History(ObservableList<State> history) { this.index = new SimpleIntegerProperty(0); this.history = new SimpleListProperty<>(history); this.inProgress = State.defaultState(); BooleanProperty prevProperty = new SimpleBooleanProperty(); prevProperty.bind(this.history.emptyProperty().or(index.isEqualTo(0)).not()); BooleanProperty nextProperty = new SimpleBooleanProperty(); nextProperty.bind(this.history.emptyProperty().or(index.greaterThanOrEqualTo(this.history.sizeProperty())).not()); this.hasPrevious = prevProperty; this.hasNext = nextProperty; } /** * Applies the <code>State</code> at the given index to the <code>GUI</code> and sets the index pointer to * <code>index</code>. The <code>index</code> must be non-negative and smaller than or equal to {@link #getSize()}. * If <code>index</code> is equal to {@link #getSize()} the 'in Progress' <code>State</code> will be applied to the * <code>GUI</code>. * * @param gui * the <code>GUI</code> to which the <code>State</code> is to be applied * @param index * the new <code>index</code> */ public void apply(GUI gui, int index) { if (!(0 <= index && index <= getSize())) { LOG.warning(() -> String.format("Invalid history index %d", index)); return; } indexProperty().set(index); if (getIndex() == getSize()) { inProgress.applyTo(gui); } else { history.get(getIndex()).applyTo(gui); } } /** * If possible, advances the index by one and applies the new <code>State</code> to the <code>GUI</code>. * If the new index is the size of the list the previously stored in-progress <code>State</code> of the * <code>GUI</code> is applied. * * @param gui * the <code>GUI</code> to which the <code>State</code> is to be applied */ public void applyNext(GUI gui) { if (getIndex() == getSize()) { return; } index.setValue(getIndex() + 1); if (getIndex() == getSize()) { inProgress.applyTo(gui); } else { history.get(getIndex()).applyTo(gui); } } /** * If possible, regresses the index by one and applies the new <code>State</code> to the <code>GUI</code>. * If the index was the size of the list (indicating that the current <code>State</code> of the <code>GUI</code> * if not one of the archived states) the current <code>State</code> is stored. * * @param gui * the <code>GUI</code> to which the <code>State</code> is to be applied */ public void applyPrevious(GUI gui) { if (getIndex() == 0) { return; } if (getIndex() == getSize()) { inProgress = State.of(gui); } index.setValue(getIndex() - 1); history.get(getIndex()).applyTo(gui); } /** * Adds the current state of the <code>GUI</code> to the <code>History</code> and sets the index to the size of the * list (indicating that the current state of the GUI is not one of the archived states). * * @param gui * the <code>GUI</code> whose <code>State</code>s are to be saved */ public void storeCurrent(GUI gui) { State currentState = State.of(gui); if (history.isEmpty() || !history.get(getSize() - 1).equals(currentState)) { history.add(currentState); index.setValue(getSize()); } } /** * Optionally (if the deserialization is successful) loads a <code>History</code> from the given <code>File</code>. * * @param file * the <code>File</code> to load the <code>History</code> from * @return optionally the loaded <code>History</code> * @throws IOException * if there is an <code>IOException</code> accessing the <code>file</code> */ public static Optional<History> load(File file) throws IOException { try (InputStream is = new BufferedInputStream(new FileInputStream(file))) { return load(is); } } /** * Optionally (if the deserialization is successful) loads a <code>History</code> from the given * <code>InputStream</code>. * * @param stream * the <code>InputStream</code> to load the <code>History</code> from * @return optionally the loaded <code>History</code> */ public static Optional<History> load(InputStream stream) { History history; try { history = (History) serializer.fromXML(stream); history.history.stream().filter(state -> GUI.isDumpGraph(state.getCmdArgs())).forEach(state -> { GraphvizParser p = new GraphvizParser(state.getOutput()); state.setTreeViewTabs(p.call().stream().map(GUI::getTreeTableViewTab).collect(Collectors.toList())); }); } catch (XStreamException | ClassCastException e) { LOG.log(Level.WARNING, e, () -> "History deserialization failed."); return Optional.empty(); } return Optional.of(history); } /** * Stores this <code>History</code> in serialized form in the given <code>File</code>. If <code>file</code> exists * it will be overwritten. * * @param file * the <code>File</code> to store this <code>History</code> in * @throws IOException * if an <code>IOException</code> occurs accessing the file */ public void store(File file) throws IOException { try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { store(os); } } /** * Writes this <code>History</code> in serialized form to the given <code>OutputStream</code>. * * @param stream * the <code>OutputStream</code> to write to */ public void store(OutputStream stream) { serializer.toXML(this, stream); } /** * Returns the number of saved <code>State</code>s. This does not include the 'in Progress' state of the GUI. * * @return the size of the <code>History</code> */ public int getSize() { return history.size(); } /** * Returns the size property. * * @return the size property */ public ReadOnlyIntegerProperty sizeProperty() { return history.sizeProperty(); } /** * Returns the current index. If this method returns {@link #getSize()} the index is pointing to the 'in Progress' * state of the GUI. * * @return the current index */ public int getIndex() { return index.get(); } /** * Returns the index property. * * @return the index property */ public IntegerProperty indexProperty() { return index; } /** * Returns whether there is a <code>State</code> before the one currently pointed at by the index. * * @return whether there is a previous <code>State</code> */ public boolean hasPrevious() { return hasPrevious.get(); } /** * Returns the previous property. * * @return the previous property */ public ReadOnlyBooleanProperty hasPreviousProperty() { return hasPrevious; } /** * Returns whether there is a <code>State</code> (including the 'in Progress' state) after the one currently * pointed at by the index. * * @return whether there is a next <code>State</code> */ public boolean hasNext() { return hasNext.get(); } /** * Returns the next property. * * @return the next property */ public ReadOnlyBooleanProperty hasNextProperty() { return hasNext; } /** * Returns a hash of the fields that are included in the output of {@link #store(File)} or * {@link #store(OutputStream)}. * * @return the hash code */ public int storeHash() { int hashCode = 1; for (State state : history) { hashCode = 31 * hashCode + (state == null ? 0 : state.storeHash()); } return hashCode; } }