| /*=============================================================================# |
| # Copyright (c) 2005, 2020 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.status.eplatform.EStatusUtils; |
| 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.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(EStatusUtils.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(EStatusUtils.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; |
| } |
| |
| } |