/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.springsource.ide.eclipse.commons.internal.core.commandhistory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.springsource.ide.eclipse.commons.core.Entry;
import org.springsource.ide.eclipse.commons.core.ICommandHistory;
import org.springsource.ide.eclipse.commons.internal.core.CorePlugin;
/**
* This provides a light-weight command history that is persisted in the user's
* workspace.
* @author Andrew Eisenberg
* @author Christian Dupuis
* @author Kris De Volder
* @since 2.5.0
*/
public class CommandHistory implements Iterable<Entry>, ICommandHistory {
/**
* Our ISaveParticipant, responsible for persisting the CommandHistory in
* the workspace.
*/
private class CommandHistorySaver implements ISaveParticipant {
public void doneSaving(ISaveContext context) {
}
public void prepareToSave(ISaveContext context) throws CoreException {
}
public void rollback(ISaveContext context) {
}
public void saving(ISaveContext context) throws CoreException {
if (isDirty()) {
try {
save(getSavePath());
}
catch (Exception e) {
throw new CoreException(CorePlugin.createErrorStatus("Couldn't save command history", e));
}
context.needSaveNumber();
}
}
}
private static final String HISTORY_FILE_NAME = ".commandhistory";
/**
* If the number of history entries exceeds maxSize, older items will be
* automatically dropped.
*/
private int maxSize = DEFAULT_MAX_SIZE;
private boolean isDirty;
private LinkedList<Entry> history = new LinkedList<Entry>();
private final String natureId;
private final String historyId;
public CommandHistory(String historyId, String natureId) {
this.natureId = natureId;
this.historyId = historyId;
}
/**
* Create an "auto saving" CommandHistory and register it as a save
* participant with the workspace.
*/
public CommandHistory(String historyId, String natureId, boolean persist) throws CoreException {
this.natureId = natureId;
this.historyId = historyId;
if (persist) {
CommandHistorySaver saver = new CommandHistorySaver();
// Method addSaveParticipant(String, ISaveParticipant) is deprecated
// on 3.6, but the alternative does not exist in 3.5
ResourcesPlugin.getWorkspace().addSaveParticipant(CorePlugin.getDefault(), saver);
load(getSavePath());
}
}
/**
* Add an element to the history. If a similar element already exists in the
* history, the older element is removed before adding the new one.
* <p>
* Adding an element beyond the maxSize will result in the oldest item being
* discarded.
*/
public void add(Entry entry) {
history.remove(entry);
history.addFirst(entry);
discardOldEntries();
isDirty = true;
}
public void clear() {
history.clear();
isDirty = true;
}
/**
* Throw away old entries until our size obeys the maxSize constraint.
*/
private void discardOldEntries() {
while (size() > getMaxSize()) {
history.removeLast();
}
}
/**
* Return the last added element.
*/
public Entry getLast() {
// elements in backing collection are actually in inverse order
return history.getFirst();
}
/**
* @return The number of items in the history is limited to.
*/
public int getMaxSize() {
return maxSize;
}
/**
* Retrieve a List of most recent entries, (most recent first).
* @param limit Return at most this many elements.
*/
public List<Entry> getRecentValid(int limit) {
ArrayList<Entry> result = new ArrayList<Entry>(limit);
for (Entry entry : validEntries()) {
if (limit-- <= 0) {
return result;
}
result.add(entry);
}
return result;
}
private IPath getSavePath() {
return CorePlugin.getDefault().getStateLocation().append(historyId + HISTORY_FILE_NAME);
}
/**
* Did this history get changed since it was last saved?
*/
public boolean isDirty() {
return isDirty;
}
public boolean isEmpty() {
return history.isEmpty();
}
/**
* Iterator that starts from the most recently added element.
*/
public Iterator<Entry> iterator() {
return history.iterator();
}
/**
* Load history contents from a file. If the file does not exist we silently
* assume the history should be empty.
* <p>
* If there are errors loading file we keep the current history and log an
* exception.
*/
public void load(File file) {
try {
if (file.exists()) {
FileInputStream fIn = new FileInputStream(file);
ObjectInputStream oIn = null;
try {
oIn = new ObjectInputStream(fIn);
load(oIn);
isDirty = false;
}
finally {
if (oIn != null) {
oIn.close();
}
else if (fIn != null) {
fIn.close();
}
}
}
}
catch (Exception e) {
CorePlugin.log("Could not restore the command history", e);
if (file.exists()) {
// File is corrupt...
file.delete();
}
}
}
private void load(IPath savePath) {
load(savePath.toFile());
}
/**
* Load the state from an InputOutputStream, let someone else worry where
* this stream came from and how to handle error / exception making sure the
* stream is closed no matter what.
* @throws ClassNotFoundException
* @throws IOException
*/
private void load(ObjectInputStream in) throws IOException, ClassNotFoundException {
int newMaxSize = in.readInt();
Entry[] elements = new Entry[in.readInt()];
for (int i = 0; i < elements.length; i++) {
elements[i] = (Entry) in.readObject();
}
// We could do this in one go, but it is slightly nicer not to modify
// the state of this history object until we are successful reading
// all the elements.
maxSize = newMaxSize;
history = new LinkedList<Entry>();
for (Entry element : elements) {
history.add(element);
}
isDirty = false;
}
/**
* Save this history to a file
* @throws IOException
*/
public void save(File file) throws IOException {
if (isDirty()) {
FileOutputStream fOut = new FileOutputStream(file);
ObjectOutputStream oOut = null;
try {
oOut = new ObjectOutputStream(fOut);
save(oOut);
isDirty = false;
}
finally {
if (oOut != null) {
oOut.close();
}
else if (fOut != null) {
fOut.close();
}
}
}
}
/**
* Save this history to a file
* @throws IOException
*/
private void save(IPath file) throws IOException {
save(file.toFile());
}
/**
* Save the state to an ObjectOutputStream, let someone else worry where
* this stream came from and how to handle error / exception making sure the
* stream is closed no matter what.
*/
private void save(ObjectOutputStream out) throws IOException {
out.writeInt(maxSize);
out.writeInt(size());
for (Entry entry : history) {
out.writeObject(entry);
}
}
/**
* Set the maxSize value. This will limit the number of items that will be
* retained in the history. When the limit is exceeded, older items are
* discarded.
*/
public void setMaxSize(int max) {
Assert.isLegal(max > 0);
this.maxSize = max;
discardOldEntries();
isDirty = true;
}
public int size() {
return history.size();
}
/**
* Returns an array of the elements in the history, with the newest item at
* position 0.
*/
public Entry[] toArray() {
Entry[] result = new Entry[size()];
int i = 0;
for (Entry entry : this) {
result[i++] = entry;
}
return result;
}
/**
* @return an Iterable (suitable for "foreach" iteration) providing only
* Entry's who's projects are valid open projects in the workspace.
*/
public Iterable<Entry> validEntries() {
return new ValidProjectFilter(this, natureId);
}
}