/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.logging.Level; import javax.swing.AbstractListModel; import javax.swing.SwingUtilities; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ProgressListener; /** {@link Runnable}s implementing this class can be execute in a dedicated thread * (single thread pool) and automatically display their progress in the status bar. * To use this class, define a property "gui.progress.KEY.label" in the GUI * properties file, and pass KEY to the constructor. Then, from within the * {@link #run()} method, use {@link #getProgressListener()} to report any * progress the task makes. * * @author Simon Fischer */ public abstract class ProgressThread implements Runnable { /** Task currently being executed. */ private static ProgressThread current = null; protected static final class QueueListModel extends AbstractListModel { private static final long serialVersionUID = 1L; private List<ProgressThread> queue = new ArrayList<ProgressThread>(); @Override public Object getElementAt(int index) { return queue.get(index); } @Override public int getSize() { return queue.size(); } private void add(final ProgressThread thread) { SwingUtilities.invokeLater(new Runnable() { public void run() { queue.add(thread); fireIntervalAdded(this, queue.size()-1, queue.size()-1); } }); } private void remove(final ProgressThread thread) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { int index = queue.indexOf(thread); queue.remove(index); fireIntervalRemoved(this, index, index); } }); } private void jobCancelled(ProgressThread pt) { int index = queue.indexOf(pt); fireContentsChanged(this, index, index); } } private static ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "ProgressThread"); thread.setDaemon(true); thread.setPriority(Thread.MIN_PRIORITY); return thread; } }); static final QueueListModel QUEUE_MODEL = new QueueListModel(); private final ProgressDisplay display; private final String name; private boolean runInForeground; public ProgressThread(String i18nKey) { this(i18nKey, false); } public ProgressThread(String i18nKey, boolean runInForeground) { this.name = I18N.getMessage(I18N.getGUIBundle(), "gui.progress."+i18nKey+".label"); this.display = new ProgressDisplay(name, this); this.runInForeground = runInForeground; } public String getName() { return name; } public String toString() { return name + (cancelled ? " (cancelled)" : ""); } public ProgressListener getProgressListener() { checkCancelled(); return display.getListener(); } public ProgressDisplay getDisplay() { return display; } /** Note that this method has nothing to do with Thread.strart. It merely enqueues * this Runnable in the Executor's queue. */ public void start() { EXECUTOR.execute(makeWrapper()); } /** Enqueues this task and waits for its completion. If you call this method, you probably * want to set the runInForeground flag in the constructor to true. */ public void startAndWait() { try { EXECUTOR.submit(makeWrapper()).get(); } catch (InterruptedException e) { LogService.getRoot().log(Level.SEVERE, "Cannot execute '"+name+"'.", e); } catch (ExecutionException e) { LogService.getRoot().log(Level.SEVERE, "Cannot execute '"+name+"'.", e); } } /** Creates a wrapper that executes this class' run method, sets {@link #current} and subsequently * removes it from the list of pending tasks and shows a {@link ProgressThreadDialog} * if necessary. As a side effect, calling this method also results in adding * this ProgressThread to the list of pending tasks. * */ private Runnable makeWrapper() { QUEUE_MODEL.add(this); return new Runnable() { @Override public void run() { synchronized (LOCK) { if (cancelled) { LogService.getRoot().info("Task "+getName()+" was cancelled."); return; } started = true; } try { current = ProgressThread.this; if (runInForeground) { SwingUtilities.invokeLater(new Runnable() { public void run() { if (!ProgressThreadDialog.getInstance().isVisible()) { ProgressThreadDialog.getInstance().setVisible(true); } }; }); } ProgressThread.this.run(); } catch (ProgressThreadStoppedException e) { LogService.getRoot().fine("Progress thread "+getName()+" aborted (cancelled)."); } catch (Exception e) { LogService.getRoot().log(Level.WARNING, "Error executing background job '"+name+"': "+e, e); SwingTools.showSimpleErrorMessage("error_executing_background_job", e, name, e); } finally { if (!ProgressThread.this.isCancelled()) { ProgressThread.this.getProgressListener().complete(); } QUEUE_MODEL.remove(ProgressThread.this); current = null; } } }; } public static ProgressThread getCurrent() { return current; } private static final Object LOCK = new Object(); private boolean cancelled = false; /** True if the thread is started. (Remains true after cancelling.) */ private boolean started = false; /** Returns true if the thread was cancelled. */ public final boolean isCancelled() { return cancelled; } /** If the thread is currently active, calls {@link #executionCancelled()} to notify children. * If not active, removes the thread from the queue so it won't become active. */ public final void cancel() { synchronized (LOCK) { cancelled = true; if (started) { executionCancelled(); QUEUE_MODEL.jobCancelled(this); } else { QUEUE_MODEL.remove(this); } } } /** Subclasses can implemented this method if they want to be notified about cancellation of this thread. * In most cases, this is not necessary. Subclasses can ask {@link #isCancelled()} whenever cancelling * is possible, or, even easier, directly call {@link #checkCancelled()}. */ protected void executionCancelled() { } /** If cancelled, throws a RuntimeException to stop the thread. */ protected void checkCancelled() throws ProgressThreadStoppedException { if (cancelled) { throw new ProgressThreadStoppedException(); } } }