blob: edbee4d1085a2c317f9a48aa9ba485cfc26e0364 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2005, 2019 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.nico.core.runtime;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.debug.core.IStreamListener;
import org.eclipse.debug.core.model.IStreamMonitor;
import org.eclipse.osgi.util.NLS;
import org.eclipse.statet.jcommons.status.ErrorStatus;
import org.eclipse.statet.jcommons.status.OkStatus;
import org.eclipse.statet.jcommons.status.ProgressMonitor;
import org.eclipse.statet.jcommons.status.Status;
import org.eclipse.statet.jcommons.status.Statuses;
import org.eclipse.statet.jcommons.ts.core.Tool;
import org.eclipse.statet.ecommons.io.FileUtil;
import org.eclipse.statet.ecommons.io.FileUtil.ReadTextFileOperation;
import org.eclipse.statet.ecommons.io.FileUtil.ReaderAction;
import org.eclipse.statet.ecommons.io.FileUtil.WriteTextFileOperation;
import org.eclipse.statet.ecommons.preferences.core.util.PreferenceUtils;
import org.eclipse.statet.ecommons.runtime.core.util.StatusUtils;
import org.eclipse.statet.internal.nico.core.Messages;
import org.eclipse.statet.internal.nico.core.preferences.HistoryPreferences;
import org.eclipse.statet.nico.core.NicoCore;
import org.eclipse.statet.nico.core.NicoCoreMessages;
import org.eclipse.statet.nico.core.NicoPreferenceNodes;
/**
* Command history.
*/
public class History {
private int maxSize= 10000; // is usually overwritten by the preferences
private int currentSize= 0;
private volatile Entry newest;
private volatile Entry oldest;
private final ListenerList listeners= new ListenerList(ListenerList.IDENTITY);
private final ReentrantReadWriteLock lock= new ReentrantReadWriteLock();
private final ToolProcess process;
private IPreferenceChangeListener preferenceListener;
private HistoryPreferences currentPreferences;
private final Map<SubmitType, IStreamListener> streamListeners= new EnumMap<>(SubmitType.class);
private volatile Entry[] arrayCache;
/**
* An entry of this history.
*/
public final class Entry {
private final String command;
private final long timeStamp;
private final SubmitType submitTypes;
private final int isEmpty;
private volatile Entry older;
private volatile Entry newer;
private Entry(final Entry older, final String command, final long stamp, final SubmitType submitType) {
this.command= command;
this.isEmpty= createCommandMarker(command);
this.timeStamp= stamp;
this.submitTypes= submitType;
this.older= older;
if (older != null) {
older.newer= this;
}
}
public String getCommand() {
return this.command;
}
public long getTimeStamp() {
return this.timeStamp;
}
public SubmitType getSubmitType() {
return this.submitTypes;
}
/**
* Returns offset of first non-blank char.
* If no such char, or first char indicates a line comment, it returns -1-offset
*/
public int getCommandMarker() {
return this.isEmpty;
}
public Entry getNewer() {
return this.newer;
}
public Entry getOlder() {
return this.older;
}
/**
* Returns the history, this entry belong to.
*
* @return the history.
*/
public History getHistory() {
return History.this;
}
private Entry dispose() {
if (this.newer != null) {
this.newer.older= null;
}
return this.newer;
}
}
History(final ToolProcess process) {
this.process= process;
this.preferenceListener= new IPreferenceChangeListener() {
@Override
public void preferenceChange(final PreferenceChangeEvent event) {
checkSettings(false);
}
};
PreferenceUtils.getInstancePrefs().addPreferenceNodeListener(
NicoPreferenceNodes.CAT_HISTORY_QUALIFIER, this.preferenceListener );
checkSettings(false);
}
void init() {
final ToolController controller= this.process.getController();
if (controller != null) {
final ToolStreamProxy streams= controller.getStreams();
final EnumSet<SubmitType> set= SubmitType.getDefaultSet();
for (final SubmitType submitType : set) {
final IStreamListener listener= new IStreamListener() {
@Override
public void streamAppended(final String text, final IStreamMonitor monitor) {
if ((((ToolStreamMonitor) monitor).getMeta() & ConsoleService.META_HISTORY_DONTADD) == 0) {
addCommand(text, submitType);
}
}
};
this.streamListeners.put(submitType, listener);
streams.getInputStreamMonitor().addListener(listener, EnumSet.of(submitType));
}
}
}
void dispose() {
if (this.preferenceListener != null) {
PreferenceUtils.getInstancePrefs().addPreferenceNodeListener(
NicoPreferenceNodes.CAT_HISTORY_QUALIFIER, this.preferenceListener );
this.preferenceListener= null;
}
}
public final Lock getReadLock() {
return this.lock.readLock();
}
private void checkSettings(final boolean force) {
final HistoryPreferences prefs= new HistoryPreferences(PreferenceUtils.getInstancePrefs());
synchronized (this) {
if (!force && prefs.equals(this.currentPreferences)) {
return;
}
this.currentPreferences= prefs;
this.lock.writeLock().lock();
}
try {
this.maxSize= prefs.getLimitCount();
if (this.currentSize > this.maxSize) {
trimSize();
fireCompleteChange();
}
}
finally {
this.lock.writeLock().unlock();
}
}
private void trimSize() {
while (this.currentSize > this.maxSize) {
this.oldest= this.oldest.dispose();
this.currentSize--;
}
}
private static class HistoryData {
Entry oldest;
Entry newest;
int size;
}
/**
* Load the history from a text file. Previous entries are removed.
*
* Note: The thread can be blocked because of workspace operations. So
* it is a good idea, that the user have the chance to cancel the action.
*
* @param file, type must be supported by IFileUtil impl.
* @param charset the charset (if not detected automatically)
* @param forceCharset use always the specified charset
* @param m
*
* @throws OperationCanceledException
*/
public Status load(final Object file, final String charset, final boolean forceCharset,
final ProgressMonitor m) {
m.beginTask(NicoCoreMessages.LoadHistoryJob_label, 4);
try {
final FileUtil fileUtil= FileUtil.getFileUtil(file);
final HistoryData exch= new HistoryData();
final ReaderAction action= new ReaderAction() {
@Override
public void run(final BufferedReader reader, final IProgressMonitor monitor) throws IOException, CoreException {
long timeStamp= fileUtil.getTimeStamp(new SubProgressMonitor(monitor, 1));
if (timeStamp < 0) {
timeStamp= System.currentTimeMillis();
}
if (reader.ready()) {
String line= reader.readLine();
timeStamp= checkTimeStamp(line, timeStamp);
exch.oldest= new Entry(null, line, timeStamp, null);
exch.newest= exch.oldest;
exch.size= 1;
final int maxSize= History.this.maxSize;
while (reader.ready()) {
line= reader.readLine();
timeStamp= checkTimeStamp(line, timeStamp);
exch.newest= new Entry(exch.newest, line, timeStamp, null);
if (exch.size < maxSize) {
exch.size++;
}
else {
exch.oldest= exch.oldest.dispose();
}
}
}
monitor.done();
}
};
final ReadTextFileOperation op= fileUtil.createReadTextFileOp(action);
op.setCharset(charset, forceCharset);
op.doOperation(StatusUtils.convert(m.newSubMonitor(3)));
m.beginSubTask(NLS.bind(Messages.LoadHistory_AllocatingTask_label, this.process.getLabel(Tool.DEFAULT_LABEL)));
this.lock.writeLock().lock();
try {
this.oldest= exch.oldest;
this.newest= exch.newest;
this.currentSize= exch.size;
if (this.currentSize > this.maxSize) {
trimSize();
}
fireCompleteChange();
}
finally {
this.lock.writeLock().unlock();
}
return new OkStatus(NicoCore.BUNDLE_ID,
NLS.bind(Messages.LoadHistory_ok_message, fileUtil.getLabel()) );
}
catch (final CoreException e) {
return new ErrorStatus(NicoCore.BUNDLE_ID,
NLS.bind(Messages.LoadHistory_error_message,
this.process.getLabel(Tool.LONG_LABEL),
file.toString() ),
e );
}
}
/**
* Allows to parse for timestamp when loading a history (from file)
*
* @param line line to parse
* @param current currently used timestamp
* @return new timestamp or current from param
*/
protected long checkTimeStamp(final String line, final long current) {
return current;
}
/**
* Save the history to a text file.
*
* Note: The thread can be blocked because of workspace operations. So
* it is a good idea, that the user have the chance to cancel the action.
*
* @param file, type must be supported by IFileUtil impl.
* @param mode allowed: EFS.OVERWRITE, EFS.APPEND
* @param charset the charset (if not appended)
* @param forceCharset use always the specified charset
* @param m
*
* @throws OperationCanceledException
*/
public Status save(final Object file, final int mode, final String charset, final boolean forceCharset,
final ProgressMonitor m) throws OperationCanceledException {
return save(file, mode, charset, forceCharset, null, m);
}
/**
* Save the history to a text file.
*
* Note: The thread can be blocked because of workspace operations. So
* it is a good idea, that the user have the chance to cancel the action.
*
* @param file, type must be supported by IFileUtil impl.
* @param mode allowed: EFS.OVERWRITE, EFS.APPEND
* @param charset the charset (if not appended)
* @param forceCharset use always the specified charset
* @param submitTypes sources to export
* @param m
*
* @throws OperationCanceledException
*/
public Status save(final Object file, final int mode, final String charset, final boolean forceCharset,
final Set<SubmitType> submitTypes, final ProgressMonitor m)
throws OperationCanceledException {
m.beginTask(NicoCoreMessages.SaveHistoryJob_label, 4);
try {
final FileUtil fileUtil= FileUtil.getFileUtil(file);
final String newLine= this.process.getWorkspaceData().getLineSeparator();
StringBuilder buffer= new StringBuilder(this.currentSize * 10);
Entry e= this.oldest;
while (e != null) {
if (m.isCanceled()) {
return Statuses.CANCEL_STATUS;
}
if (submitTypes == null || e.submitTypes == null
|| submitTypes.contains(e.submitTypes)) {
buffer.append(e.command);
buffer.append(newLine);
}
e= e.newer;
}
final String content= buffer.toString();
buffer= null;
if (m.isCanceled()) {
throw new OperationCanceledException();
}
m.addWorked(1);
final WriteTextFileOperation op= fileUtil.createWriteTextFileOp(content);
op.setCharset(charset, forceCharset);
op.setFileOperationMode(mode);
op.doOperation(StatusUtils.convert(m.newSubMonitor(2)));
return new OkStatus(NicoCore.BUNDLE_ID,
NLS.bind(Messages.SaveHistory_ok_message, fileUtil.getLabel()) );
}
catch (final CoreException e) {
return new ErrorStatus(NicoCore.BUNDLE_ID,
NLS.bind(Messages.SaveHistory_error_message,
this.process.getLabel(),
file.toString() ),
e );
}
}
final void addCommand(final String command, final SubmitType submitType) {
assert(command != null);
final long stamp= System.currentTimeMillis();
Entry removedEntry= null;
Entry newEntry= null;
this.lock.writeLock().lock();
try {
newEntry= new Entry(this.newest, command, stamp, submitType);
if (this.newest != null) {
this.newest.newer= newEntry;
}
else {
this.oldest= newEntry;
}
this.newest= newEntry;
if (this.currentSize == this.maxSize) {
removedEntry= this.oldest;
this.oldest= this.oldest.dispose();
}
else {
this.currentSize++;
}
final Object[] listeners= this.listeners.getListeners();
for (final Object obj : listeners) {
final IHistoryListener listener= (IHistoryListener) obj;
if (removedEntry != null) {
listener.entryRemoved(this, removedEntry);
}
listener.entryAdded(this, newEntry);
}
}
finally {
this.lock.writeLock().unlock();
}
}
/**
* Return the newest history entry.
*
* @return newest entry
* or <code>null</null>, if history is empty.
*/
public final Entry getNewest() {
return this.newest;
}
/**
* Return an array with all entries.
* <p>
* Make shure, that you have a read lock.
*
* @return array with all entries
* or an array with length 0, if history is empty.
*/
public final Entry[] toArray() {
Entry[] array= this.arrayCache;
if (array != null) {
return array;
}
array= new Entry[this.currentSize];
Entry e= this.oldest;
for (int i= 0; i < array.length; i++) {
array[i]= e;
e= e.newer;
}
return array;
}
/**
* Adds the given listener to this history.
* Has no effect if an identical listener is already registered.
*
* @param listener the listener
*/
public final void addListener(final IHistoryListener listener) {
this.listeners.add(listener);
}
/**
* Removes the given listener from this history.
* Has no effect if an identical listener was not already registered.
*
* @param listener the listener
*/
public final void removeListener(final IHistoryListener listener) {
this.listeners.remove(listener);
}
private void fireCompleteChange() {
this.arrayCache= toArray();
for (final Object obj : this.listeners.getListeners()) {
((IHistoryListener) obj).completeChange(this, this.arrayCache);
}
this.arrayCache= null;
}
/**
* Checks, if this command is empty or an command
*/
protected int createCommandMarker(final String command) {
final int length= command.length();
for (int i= 0; i < length; i++) {
final char c= command.charAt(i);
switch(c) {
case ' ':
case '\t':
continue;
case '#':
return -i-1;
default:
return i;
}
}
return -length-1;
}
}