* full support for multiple checksum types (SFV, MD5, SHA-1)

notes:
* updated to MigLayout 3.6.3
* better exception handling in *TransferablePolicy
* added checksum toggle button and artwork
* poperly cancel computation tasks on reset
* better "Total Progress" visibility behaviour
* improved checksum table model classes, better update/repaint behaviour
This commit is contained in:
Reinhard Pointner 2009-02-15 12:20:43 +00:00
parent 9d7af8bd96
commit 87e8d830ce
46 changed files with 1685 additions and 861 deletions

BIN
fw/button.checksum.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -26,7 +26,7 @@ import net.sourceforge.filebot.ui.panel.analyze.AnalyzePanel;
import net.sourceforge.filebot.ui.panel.episodelist.EpisodeListPanel;
import net.sourceforge.filebot.ui.panel.list.ListPanel;
import net.sourceforge.filebot.ui.panel.rename.RenamePanel;
import net.sourceforge.filebot.ui.panel.sfv.SfvPanel;
import net.sourceforge.filebot.ui.panel.sfv.ChecksumPanel;
import net.sourceforge.filebot.ui.panel.subtitle.SubtitlePanel;
import net.sourceforge.tuned.MessageBus;
import net.sourceforge.tuned.MessageHandler;
@ -85,7 +85,7 @@ public class FileBotWindow extends JFrame implements ListSelectionListener {
panels.add(new AnalyzePanel());
panels.add(new EpisodeListPanel());
panels.add(new SubtitlePanel());
panels.add(new SfvPanel());
panels.add(new ChecksumPanel());
return panels;
}

View File

@ -29,7 +29,7 @@ public class FileTransferableMessageHandler implements MessageHandler {
@Override
public void handle(String topic, Object... messages) {
public void handle(String topic, Object... messages) throws Exception {
// switch to panel
MessageBus.getDefault().publish("panel", panel);

View File

@ -62,6 +62,7 @@ class FileTreeTransferablePolicy extends BackgroundFileTransferablePolicy<Abstra
protected void load(List<File> files) {
try {
for (File file : files) {
// use fast file to minimize system calls like length(), isDirectory(), isFile(), ...
AbstractTreeNode node = getTreeNode(new FastFile(file.getPath()));
// publish on EDT

View File

@ -11,9 +11,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sourceforge.filebot.FileBotUtilities;
import net.sourceforge.filebot.torrent.Torrent;
import net.sourceforge.filebot.ui.FileBotList;
@ -44,7 +41,7 @@ class FileListTransferablePolicy extends FileTransferablePolicy {
@Override
protected void load(List<File> files) {
protected void load(List<File> files) throws IOException {
// set title based on parent folder of first file
list.setTitle(FileUtilities.getFolderName(files.get(0).getParentFile()));
@ -70,26 +67,21 @@ class FileListTransferablePolicy extends FileTransferablePolicy {
}
private void loadTorrents(List<File> torrentFiles) {
try {
List<Torrent> torrents = new ArrayList<Torrent>(torrentFiles.size());
for (File file : torrentFiles) {
torrents.add(new Torrent(file));
private void loadTorrents(List<File> torrentFiles) throws IOException {
List<Torrent> torrents = new ArrayList<Torrent>(torrentFiles.size());
for (File file : torrentFiles) {
torrents.add(new Torrent(file));
}
if (torrentFiles.size() == 1) {
list.setTitle(FileUtilities.getNameWithoutExtension(torrents.get(0).getName()));
}
for (Torrent torrent : torrents) {
for (Torrent.Entry entry : torrent.getFiles()) {
list.getModel().add(FileUtilities.getNameWithoutExtension(entry.getName()));
}
if (torrentFiles.size() == 1) {
list.setTitle(FileUtilities.getNameWithoutExtension(torrents.get(0).getName()));
}
for (Torrent torrent : torrents) {
for (Torrent.Entry entry : torrent.getFiles()) {
list.getModel().add(FileUtilities.getNameWithoutExtension(entry.getName()));
}
}
} catch (IOException e) {
// should not happen
Logger.getLogger("global").log(Level.SEVERE, e.toString(), e);
}
}

View File

@ -13,6 +13,7 @@ import static net.sourceforge.tuned.FileUtilities.getNameWithoutExtension;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
@ -45,7 +46,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
@Override
public boolean accept(Transferable tr) {
public boolean accept(Transferable tr) throws Exception {
return tr.isDataFlavorSupported(stringFlavor) || super.accept(tr);
}
@ -57,7 +58,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
@Override
public void handleTransferable(Transferable tr, TransferAction action) {
public void handleTransferable(Transferable tr, TransferAction action) throws Exception {
if (action == TransferAction.PUT) {
clear();
}
@ -121,7 +122,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
@Override
protected void load(List<File> files) {
protected void load(List<File> files) throws FileNotFoundException {
if (containsOnly(files, LIST_FILES)) {
loadListFiles(files);
} else if (containsOnly(files, TORRENT_FILES)) {
@ -144,28 +145,24 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
}
protected void loadListFiles(List<File> files) {
try {
List<MutableString> entries = new ArrayList<MutableString>();
protected void loadListFiles(List<File> files) throws FileNotFoundException {
List<MutableString> entries = new ArrayList<MutableString>();
for (File file : files) {
Scanner scanner = new Scanner(file, "UTF-8").useDelimiter(LINE_SEPARATOR);
for (File file : files) {
Scanner scanner = new Scanner(file, "UTF-8").useDelimiter(LINE_SEPARATOR);
while (scanner.hasNext()) {
String line = scanner.next();
while (scanner.hasNext()) {
String line = scanner.next();
if (line.trim().length() > 0) {
entries.add(new MutableString(line));
}
if (line.trim().length() > 0) {
entries.add(new MutableString(line));
}
scanner.close();
}
submit(entries);
} catch (IOException e) {
Logger.getLogger("global").log(Level.SEVERE, e.toString(), e);
scanner.close();
}
submit(entries);
}

View File

@ -0,0 +1,80 @@
package net.sourceforge.filebot.ui.panel.sfv;
import static java.awt.Color.WHITE;
import static java.awt.Cursor.*;
import static java.awt.Font.DIALOG;
import static java.awt.Font.PLAIN;
import static java.awt.RenderingHints.KEY_RENDERING;
import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_RENDER_QUALITY;
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
import static java.lang.Math.max;
import static java.lang.Math.round;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JToggleButton;
import net.sourceforge.filebot.ResourceManager;
public class ChecksumButton extends JToggleButton {
private static final ImageIcon contentArea = ResourceManager.getIcon("button.checksum");
private static final ImageIcon contentAreaSelected = ResourceManager.getIcon("button.checksum.selected");
public ChecksumButton(Action action) {
super(action);
setPreferredSize(new Dimension(max(contentAreaSelected.getIconWidth(), contentArea.getIconWidth()), max(contentAreaSelected.getIconHeight(), contentArea.getIconHeight())));
setMinimumSize(getPreferredSize());
setMaximumSize(getPreferredSize());
setForeground(WHITE);
setFont(new Font(DIALOG, PLAIN, 11));
setContentAreaFilled(false);
setFocusPainted(false);
setEnabled(true);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
// set appropriate cursor
setCursor(getPredefinedCursor(enabled ? HAND_CURSOR : DEFAULT_CURSOR));
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
// paint background image in the center
if (isSelected()) {
contentAreaSelected.paintIcon(this, g2d, (int) round((getWidth() - contentAreaSelected.getIconWidth()) / (double) 2), (int) round((getHeight() - contentAreaSelected.getIconHeight()) / (double) 2));
} else {
contentArea.paintIcon(this, g2d, (int) round((getWidth() - contentArea.getIconWidth()) / (double) 2), (int) round((getHeight() - contentArea.getIconHeight()) / (double) 2));
}
Rectangle2D textBounds = g2d.getFontMetrics().getStringBounds(getText(), g2d);
// draw text in the center
g2d.drawString(getText(), round((getWidth() - textBounds.getWidth()) / 2) + 1, round(getHeight() / 2 - textBounds.getY() - textBounds.getHeight() / 2));
}
}

View File

@ -4,12 +4,15 @@ package net.sourceforge.filebot.ui.panel.sfv;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.CancellationException;
import javax.swing.SwingWorker.StateValue;
import javax.swing.event.SwingPropertyChangeSupport;
import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter;
class ChecksumCell {
@ -37,33 +40,14 @@ class ChecksumCell {
}
public ChecksumCell(String name, File root, ChecksumComputationTask computationTask) {
public ChecksumCell(String name, File root, ChecksumComputationTask task) {
this.name = name;
this.root = root;
this.task = computationTask;
this.hashes = new EnumMap<HashType, String>(HashType.class);
this.task = task;
// forward property change events
task.addPropertyChangeListener(new SwingWorkerPropertyChangeAdapter() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
super.propertyChange(evt);
pcs.firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
@Override
protected void done(PropertyChangeEvent evt) {
try {
hashes = task.get();
} catch (Exception e) {
error = ExceptionUtilities.getRootCause(e);
} finally {
task = null;
}
}
});
task.addPropertyChangeListener(taskListener);
}
@ -77,11 +61,25 @@ class ChecksumCell {
}
public String getChecksum(HashType type) {
if (hashes != null)
return hashes.get(type);
public String getChecksum(HashType hash) {
return hashes.get(hash);
}
public void putTask(ChecksumComputationTask computationTask) {
if (task != null) {
task.removePropertyChangeListener(taskListener);
task.cancel(true);
}
return null;
task = computationTask;
error = null;
// forward property change events
task.addPropertyChangeListener(taskListener);
// state changed to PENDING
pcs.firePropertyChange("state", null, getState());
}
@ -96,27 +94,31 @@ class ChecksumCell {
public State getState() {
if (hashes != null)
return State.READY;
if (error != null)
return State.ERROR;
switch (task.getState()) {
case PENDING:
return State.PENDING;
default:
return State.PROGRESS;
if (task != null) {
switch (task.getState()) {
case PENDING:
return State.PENDING;
default:
return State.PROGRESS;
}
}
if (error != null) {
return State.ERROR;
}
return State.READY;
}
public void dispose() {
// clear property change support first
// clear property change support
for (PropertyChangeListener listener : pcs.getPropertyChangeListeners()) {
pcs.removePropertyChangeListener(listener);
}
if (task != null) {
task.removePropertyChangeListener(taskListener);
task.cancel(true);
}
@ -132,7 +134,42 @@ class ChecksumCell {
return String.format("%s %s", name, hashes);
}
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private final PropertyChangeListener taskListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("state".equals(evt.getPropertyName())) {
if (evt.getNewValue() == StateValue.DONE)
done(evt);
// cell state changed because worker state changed
pcs.firePropertyChange("state", null, getState());
} else {
// progress events
pcs.firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
}
protected void done(PropertyChangeEvent evt) {
try {
hashes.putAll(task.get());
} catch (Exception e) {
Throwable cause = ExceptionUtilities.getRootCause(e);
// ignore cancellation
if (cause instanceof CancellationException) {
return;
}
error = cause;
} finally {
task = null;
}
}
};
private SwingPropertyChangeSupport pcs = new SwingPropertyChangeSupport(this, true);
public void addPropertyChangeListener(PropertyChangeListener listener) {

View File

@ -0,0 +1,58 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.awt.Color;
import java.awt.Component;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.SwingWorker.StateValue;
import javax.swing.table.DefaultTableCellRenderer;
import net.sourceforge.tuned.ExceptionUtilities;
public class ChecksumCellRenderer extends DefaultTableCellRenderer {
private final SwingWorkerCellRenderer progressRenderer = new SwingWorkerCellRenderer();
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
boolean pendingWorker = false;
if (value instanceof SwingWorker) {
if (((SwingWorker<?, ?>) value).getState() != StateValue.PENDING)
return progressRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
pendingWorker = true;
}
// ignore focus
super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
// restore text color
setForeground(isSelected ? table.getSelectionForeground() : table.getForeground());
setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
if (pendingWorker) {
setText("Pending...");
} else if (value == null && !isSelected) {
setBackground(derive(table.getGridColor(), 0.1f));
} else if (value instanceof Throwable) {
setText(ExceptionUtilities.getRootCauseMessage((Throwable) value));
if (!isSelected) {
setForeground(Color.RED);
}
}
return this;
}
private Color derive(Color color, float alpha) {
return new Color(((((int) (alpha * 255)) & 0xFF) << 24) & (color.getRGB() | 0xFF000000), true);
}
}

View File

@ -7,9 +7,10 @@ import static java.lang.Math.max;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@ -22,7 +23,7 @@ class ChecksumComputationService {
public static final String TASK_COUNT_PROPERTY = "taskCount";
private final List<ThreadPoolExecutor> executors = new ArrayList<ThreadPoolExecutor>();
private final Set<ThreadPoolExecutor> executors = new HashSet<ThreadPoolExecutor>(4);
private final AtomicInteger completedTaskCount = new AtomicInteger(0);
private final AtomicInteger totalTaskCount = new AtomicInteger(0);
@ -36,7 +37,12 @@ class ChecksumComputationService {
public void reset() {
synchronized (executors) {
for (ExecutorService executor : executors) {
executor.shutdownNow();
for (Runnable runnable : executor.shutdownNow()) {
// cancel all remaining tasks
if (runnable instanceof Future) {
((Future<?>) runnable).cancel(false);
}
}
}
totalTaskCount.set(0);
@ -50,7 +56,7 @@ class ChecksumComputationService {
public int getTaskCount() {
return getTotalTaskCount() - getCompletedTaskCount();
return totalTaskCount.get() - completedTaskCount.get();
}
@ -79,7 +85,10 @@ class ChecksumComputationService {
super(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DefaultThreadFactory("ChecksumComputationPool", Thread.MIN_PRIORITY));
synchronized (executors) {
executors.add(this);
if (executors.add(this) && executors.size() == 1) {
totalTaskCount.set(0);
completedTaskCount.set(0);
}
}
prestartAllCoreThreads();
@ -133,8 +142,22 @@ class ChecksumComputationService {
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
completedTaskCount.incrementAndGet();
fireTaskCountChanged();
if (isValid()) {
if (r instanceof Future && ((Future<?>) r).isCancelled()) {
totalTaskCount.decrementAndGet();
} else {
completedTaskCount.incrementAndGet();
}
fireTaskCountChanged();
}
}
protected boolean isValid() {
synchronized (executors) {
return executors.contains(this);
}
}

View File

@ -4,9 +4,12 @@ package net.sourceforge.filebot.ui.panel.sfv;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CancellationException;
import javax.swing.SwingWorker;
@ -15,48 +18,52 @@ class ChecksumComputationTask extends SwingWorker<Map<HashType, String>, Void> {
private static final int BUFFER_SIZE = 32 * 1024;
private final File file;
private final HashType type;
private final HashType hashType;
public ChecksumComputationTask(File file, HashType type) {
public ChecksumComputationTask(File file, HashType hashType) {
this.file = file;
this.type = type;
this.hashType = hashType;
}
@Override
protected Map<HashType, String> doInBackground() throws Exception {
Hash hash = type.newInstance();
if (!file.exists())
throw new FileNotFoundException("File not found");
// create hash instance
Hash hash = hashType.newHash();
// cache length for speed
long length = file.length();
if (length > 0) {
InputStream in = new FileInputStream(file);
// open file
InputStream in = new FileInputStream(file);
try {
byte[] buffer = new byte[BUFFER_SIZE];
try {
byte[] buffer = new byte[BUFFER_SIZE];
long position = 0;
int len = 0;
while ((len = in.read(buffer)) >= 0) {
position += len;
long position = 0;
int len = 0;
hash.update(buffer, 0, len);
while ((len = in.read(buffer)) >= 0) {
position += len;
hash.update(buffer, 0, len);
// update progress
setProgress((int) ((position * 100) / length));
// check abort status
if (isCancelled()) {
break;
}
// update progress
setProgress((int) ((position * 100) / length));
// check abort status
if (isCancelled() || Thread.interrupted()) {
throw new CancellationException();
}
} finally {
in.close();
}
} finally {
in.close();
}
return Collections.singletonMap(type, hash.digest());
return Collections.singletonMap(hashType, hash.digest());
}
}

View File

@ -0,0 +1,293 @@
package net.sourceforge.filebot.ui.panel.sfv;
import static net.sourceforge.filebot.ui.panel.sfv.ChecksumTableModel.*;
import static net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy.*;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.ui.FileBotPanel;
import net.sourceforge.filebot.ui.FileTransferableMessageHandler;
import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler;
import net.sourceforge.filebot.ui.transfer.LoadAction;
import net.sourceforge.filebot.ui.transfer.SaveAction;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.MessageHandler;
import net.sourceforge.tuned.ui.TunedUtilities;
public class ChecksumPanel extends FileBotPanel {
private final ChecksumComputationService computationService = new ChecksumComputationService();
private final ChecksumTable table = new ChecksumTable();
private final ChecksumTableTransferablePolicy transferablePolicy = new ChecksumTableTransferablePolicy(table.getModel(), computationService);
private final ChecksumTableExportHandler exportHandler = new ChecksumTableExportHandler(table.getModel());
private final MessageHandler messageHandler = new FileTransferableMessageHandler(this, transferablePolicy);
public ChecksumPanel() {
super("SFV", ResourceManager.getIcon("panel.sfv"));
table.setTransferHandler(new DefaultTransferHandler(transferablePolicy, exportHandler));
JPanel contentPane = new JPanel(new MigLayout("insets 0, fill", "", "[fill]10px[nogrid, bottom]4px"));
contentPane.setBorder(new TitledBorder(getPanelName()));
setLayout(new MigLayout("insets dialog, fill"));
add(contentPane, "grow");
contentPane.add(new JScrollPane(table), "grow, wrap");
contentPane.add(new JButton(loadAction), "gap left 15px");
contentPane.add(new JButton(saveAction));
contentPane.add(new JButton(clearAction), "gap right 40px");
// hash function toggle button group
ButtonGroup group = new ButtonGroup();
for (HashType hash : HashType.values()) {
JToggleButton button = new ChecksumButton(new ChangeHashTypeAction(hash));
group.add(button);
contentPane.add(button);
}
contentPane.add(new TotalProgressPanel(computationService), "gap left 35px:push, gap right 7px, hidemode 1");
// cancel and restart computations whenever the hash function has been changed
table.getModel().addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (HASH_TYPE_PROPERTY.equals(evt.getPropertyName())) {
restartComputation((HashType) evt.getNewValue());
}
}
});
// Shortcut DELETE
TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction);
}
protected void restartComputation(HashType hash) {
// cancel all running computations
computationService.reset();
ChecksumTableModel model = table.getModel();
// calculate new hashes, one executor for each checksum column
Map<File, ExecutorService> executors = new HashMap<File, ExecutorService>(4);
for (ChecksumRow row : model.rows()) {
for (ChecksumCell cell : row.values()) {
if (cell.getChecksum(hash) == null && cell.getRoot().isDirectory()) {
cell.putTask(new ChecksumComputationTask(new File(cell.getRoot(), cell.getName()), hash));
ExecutorService executor = executors.get(cell.getRoot());
if (executor == null) {
executor = computationService.newExecutor();
executors.put(cell.getRoot(), executor);
}
// start computation
executor.execute(cell.getTask());
}
}
}
// start shutdown sequence for all created executors
for (ExecutorService executor : executors.values()) {
executor.shutdown();
}
}
@Override
public MessageHandler getMessageHandler() {
return messageHandler;
}
private final SaveAction saveAction = new ChecksumTableSaveAction();
private final LoadAction loadAction = new LoadAction(transferablePolicy);
private final AbstractAction clearAction = new AbstractAction("Clear", ResourceManager.getIcon("action.clear")) {
public void actionPerformed(ActionEvent e) {
transferablePolicy.reset();
computationService.reset();
table.getModel().clear();
}
};
private final AbstractAction removeAction = new AbstractAction("Remove") {
public void actionPerformed(ActionEvent e) {
if (table.getSelectedRowCount() < 1)
return;
int firstSelectedRow = table.getSelectedRow();
// remove selected rows
table.getModel().remove(table.getSelectedRows());
// update computation service task count
computationService.purge();
// auto select next row
firstSelectedRow = Math.min(firstSelectedRow, table.getRowCount() - 1);
table.getSelectionModel().setSelectionInterval(firstSelectedRow, firstSelectedRow);
}
};
protected class ChangeHashTypeAction extends AbstractAction implements PropertyChangeListener {
private ChangeHashTypeAction(HashType hash) {
super(hash.toString());
putValue(HASH_TYPE_PROPERTY, hash);
// initialize selected state
propertyChange(new PropertyChangeEvent(this, HASH_TYPE_PROPERTY, null, table.getModel().getHashType()));
transferablePolicy.addPropertyChangeListener(this);
table.getModel().addPropertyChangeListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
table.getModel().setHashType((HashType) getValue(HASH_TYPE_PROPERTY));
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (LOADING_PROPERTY.equals(evt.getPropertyName())) {
// update enabled state
setEnabled(!(Boolean) evt.getNewValue());
} else if (HASH_TYPE_PROPERTY.equals(evt.getPropertyName())) {
// update selected state
putValue(SELECTED_KEY, evt.getNewValue() == getValue(HASH_TYPE_PROPERTY));
}
}
}
protected class ChecksumTableSaveAction extends SaveAction {
private File selectedColumn = null;
public ChecksumTableSaveAction() {
super(exportHandler);
}
@Override
public ChecksumTableExportHandler getExportHandler() {
return (ChecksumTableExportHandler) super.getExportHandler();
}
@Override
protected boolean canExport() {
return selectedColumn != null && super.canExport();
}
@Override
protected void export(File file) throws IOException {
getExportHandler().export(file, selectedColumn);
}
@Override
protected String getDefaultFileName() {
return getExportHandler().getDefaultFileName(selectedColumn);
}
@Override
protected File getDefaultFolder() {
// use the column root as default folder in the file dialog
return selectedColumn;
}
@Override
public void actionPerformed(ActionEvent e) {
List<File> options = new ArrayList<File>();
// filter out verification file columns
for (File file : table.getModel().checksumColumns()) {
if (file.isDirectory())
options.add(file);
}
// can't export anything
if (options.isEmpty()) {
return;
}
try {
if (options.size() == 1) {
// auto-select option if there is only one option
this.selectedColumn = options.get(0);
} else if (options.size() > 1) {
// user must select one option
SelectDialog<File> selectDialog = new SelectDialog<File>(SwingUtilities.getWindowAncestor(ChecksumPanel.this), options) {
@Override
protected String convertValueToString(Object value) {
return FileUtilities.getFolderName((File) value);
}
};
selectDialog.getHeaderLabel().setText("Select checksum column:");
selectDialog.setVisible(true);
this.selectedColumn = selectDialog.getSelectedValue();
}
if (this.selectedColumn != null) {
// continue if a column was selected
super.actionPerformed(e);
}
} finally {
// reset selected column
this.selectedColumn = null;
}
}
}
}

View File

@ -13,6 +13,8 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.swing.event.SwingPropertyChangeSupport;
import net.sourceforge.filebot.FileBotUtilities;
@ -53,38 +55,52 @@ class ChecksumRow {
}
protected void setState(State newValue) {
State oldValue = this.state;
this.state = newValue;
pcs.firePropertyChange("state", oldValue, newValue);
}
public ChecksumCell getChecksum(File root) {
return hashes.get(root);
}
public void put(ChecksumCell cell) {
ChecksumCell old = hashes.put(cell.getRoot(), cell);
// dispose of old map entry
if (old != null) {
old.dispose();
}
// update state immediately
updateState();
// keep state up-to-date
cell.addPropertyChangeListener(updateStateListener);
public Collection<ChecksumCell> values() {
return Collections.unmodifiableCollection(hashes.values());
}
public void updateState() {
public ChecksumCell put(ChecksumCell cell) {
ChecksumCell old = hashes.put(cell.getRoot(), cell);
// update state immediately, don't fire property change
state = getState(hashes.values());
// keep state up-to-date
cell.addPropertyChangeListener(updateStateListener);
return old;
}
public void dispose() {
// clear property change support
for (PropertyChangeListener listener : pcs.getPropertyChangeListeners()) {
pcs.removePropertyChangeListener(listener);
}
for (ChecksumCell cell : hashes.values()) {
cell.dispose();
}
hashes.clear();
name = null;
embeddedChecksum = null;
hashes = null;
state = null;
pcs = null;
}
@ -111,7 +127,7 @@ class ChecksumRow {
String checksum = cell.getChecksum(type);
if (checksum != null) {
checksumSet.add(checksum);
checksumSet.add(checksum.toLowerCase());
}
}
@ -153,8 +169,22 @@ class ChecksumRow {
private final PropertyChangeListener updateStateListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
updateState();
if ("state".equals(evt.getPropertyName())) {
setState(getState(hashes.values()));
}
}
};
private SwingPropertyChangeSupport pcs = new SwingPropertyChangeSupport(this, true);
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
}

View File

@ -2,18 +2,17 @@
package net.sourceforge.filebot.ui.panel.sfv;
import net.sourceforge.filebot.FileBotUtilities;
import net.sourceforge.tuned.ui.TunedUtilities.DragDropRowTableUI;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import net.sourceforge.filebot.FileBotUtilities;
class SfvTable extends JTable {
class ChecksumTable extends JTable {
public SfvTable() {
public ChecksumTable() {
setFillsViewportHeight(true);
setAutoCreateRowSorter(true);
setAutoCreateColumnsFromModel(true);
@ -23,12 +22,13 @@ class SfvTable extends JTable {
setRowHeight(20);
setDragEnabled(true);
setUI(new DragDropRowTableUI());
// highlight CRC32 patterns in filenames in green and with smaller font-size
setDefaultRenderer(String.class, new HighlightPatternCellRenderer(FileBotUtilities.EMBEDDED_CHECKSUM_PATTERN, "#009900", "smaller"));
setDefaultRenderer(ChecksumRow.State.class, new StateIconTableCellRenderer());
setDefaultRenderer(ChecksumCell.class, new ChecksumTableCellRenderer());
setDefaultRenderer(ChecksumRow.State.class, new StateIconCellRenderer());
setDefaultRenderer(ChecksumCell.class, new ChecksumCellRenderer());
}

View File

@ -1,115 +0,0 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Rectangle;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTable;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import net.sourceforge.tuned.ExceptionUtilities;
class ChecksumTableCellRenderer extends DefaultTableCellRenderer {
private final ProgressBarTableCellRenderer progressBarRenderer = new ProgressBarTableCellRenderer();
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, null, isSelected, false, row, column);
if (value instanceof ChecksumCell) {
ChecksumCell checksum = (ChecksumCell) value;
switch (checksum.getState()) {
case READY:
setText(checksum.getChecksum(HashType.CRC32));
break;
case PENDING:
setText("Pending ...");
break;
case ERROR:
setText(ExceptionUtilities.getMessage(checksum.getError()));
break;
default:
return progressBarRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
}
return this;
}
private static class ProgressBarTableCellRenderer extends JPanel implements TableCellRenderer {
private final JProgressBar progressBar = new JProgressBar(0, 100);
public ProgressBarTableCellRenderer() {
progressBar.setStringPainted(true);
setLayout(new BorderLayout());
add(progressBar, BorderLayout.CENTER);
setBorder(new EmptyBorder(2, 2, 2, 2));
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
ChecksumComputationTask task = ((ChecksumCell) value).getTask();
if (task != null) {
progressBar.setValue(task.getProgress());
}
if (isSelected) {
this.setBackground(table.getSelectionBackground());
} else {
this.setBackground(table.getBackground());
}
return this;
}
/**
* Overridden for performance reasons.
*/
@Override
public void repaint(long tm, int x, int y, int width, int height) {
}
/**
* Overridden for performance reasons.
*/
@Override
public void repaint(Rectangle r) {
}
/**
* Overridden for performance reasons.
*/
@Override
public void repaint() {
}
/**
* Overridden for performance reasons.
*/
@Override
public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
}
}
}

View File

@ -24,19 +24,30 @@ class ChecksumTableExportHandler extends TextFileExportHandler {
@Override
public boolean canExport() {
return model.getRowCount() > 0 && model.getChecksumColumns().size() > 0;
return model.getRowCount() > 0 && defaultColumn() != null;
}
@Override
public void export(Formatter out) {
export(out, model.getChecksumColumns().get(0));
export(out, defaultColumn());
}
@Override
public String getDefaultFileName() {
return getDefaultFileName(model.getChecksumColumns().get(0));
return getDefaultFileName(defaultColumn());
}
protected File defaultColumn() {
// select first column that is not a verification file column
for (File root : model.checksumColumns()) {
if (root.isDirectory())
return root;
}
return null;
}
@ -52,27 +63,38 @@ class ChecksumTableExportHandler extends TextFileExportHandler {
public void export(Formatter out, File column) {
out.format("; Generated by %s on %tF at %<tT%n", Settings.getApplicationName(), new Date());
HashType hashType = model.getHashType();
// print header
out.format("; Generated by %s %s on %tF at %<tT%n", Settings.getApplicationName(), Settings.getApplicationVersion(), new Date());
out.format(";%n");
out.format(";%n");
for (ChecksumRow row : model) {
//TODO select hash type
out.format("%s %s%n", row.getName(), row.getChecksum(column).getChecksum(HashType.CRC32));
// print data
VerificationFilePrinter printer = hashType.newPrinter(out);
for (ChecksumRow row : model.rows()) {
ChecksumCell cell = row.getChecksum(column);
if (cell != null) {
String hash = cell.getChecksum(hashType);
if (hash != null) {
printer.println(cell.getName(), hash);
}
}
}
}
public String getDefaultFileName(File column) {
String name = "";
StringBuilder sb = new StringBuilder();
if (column != null)
name = FileUtilities.getName(column);
sb.append(FileUtilities.getName(column));
else
sb.append("name");
if (name.isEmpty())
name = "name";
return name + ".sfv";
return sb.append(".").append(model.getHashType().getExtension()).toString();
}
}

View File

@ -2,29 +2,25 @@
package net.sourceforge.filebot.ui.panel.sfv;
import static javax.swing.event.TableModelEvent.UPDATE;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import net.sourceforge.tuned.FileUtilities;
class ChecksumTableModel extends AbstractTableModel implements Iterable<ChecksumRow> {
class ChecksumTableModel extends AbstractTableModel {
private final IndexedMap<String, ChecksumRow> rows = new IndexedMap<String, ChecksumRow>() {
@ -34,7 +30,10 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
}
};
private final List<File> columns = new ArrayList<File>(4);
private final List<File> checksumColumns = new ArrayList<File>(4);
public static final String HASH_TYPE_PROPERTY = "hashType";
private HashType hashType = HashType.SFV;
@Override
@ -64,19 +63,47 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
}
public File getColumnRoot(int columnIndex) {
return columns.get(columnIndex - 2);
protected int getColumnIndex(ChecksumCell cell) {
int index = checksumColumns.indexOf(cell.getRoot());
if (index < 0)
return -1;
// add checksum column offset
return index + 2;
}
protected File getColumnRoot(int columnIndex) {
// substract checksum column offset
return checksumColumns.get(columnIndex - 2);
}
public List<File> checksumColumns() {
return Collections.unmodifiableList(checksumColumns);
}
@Override
public int getColumnCount() {
return columns.size() + 2;
// add checksum column offset
return checksumColumns.size() + 2;
}
public List<File> getChecksumColumns() {
return Collections.unmodifiableList(columns);
protected int getRowIndex(ChecksumRow row) {
return rows.getIndexByKey(row.getName());
}
protected int getRowIndex(ChecksumCell cell) {
return rows.getIndexByKey(cell.getName());
}
public List<ChecksumRow> rows() {
return Collections.unmodifiableList(rows);
}
@ -86,6 +113,24 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
}
public void setHashType(HashType hashType) {
HashType old = this.hashType;
this.hashType = hashType;
// update table
fireTableDataChanged();
// notify listeners
pcs.firePropertyChange(HASH_TYPE_PROPERTY, old, hashType);
}
public HashType getHashType() {
return hashType;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
ChecksumRow row = rows.get(rowIndex);
@ -95,44 +140,81 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
return row.getState();
case 1:
return row.getName();
default:
return row.getChecksum(getColumnRoot(columnIndex));
}
ChecksumCell cell = row.getChecksum(getColumnRoot(columnIndex));
// empty cell
if (cell == null)
return null;
switch (cell.getState()) {
case READY:
return cell.getChecksum(hashType);
case ERROR:
return cell.getError();
default: // PENDING or PROGRESS
return cell.getTask();
}
}
@Override
public Iterator<ChecksumRow> iterator() {
return rows.iterator();
}
public void addAll(List<ChecksumCell> list) {
int firstRow = getRowCount();
public void addAll(Collection<ChecksumCell> values) {
List<ChecksumCell> replacements = new ArrayList<ChecksumCell>();
for (ChecksumCell entry : list) {
ChecksumRow row = rows.getByKey(entry.getName());
int rowCount = getRowCount();
int columnCount = getColumnCount();
for (ChecksumCell cell : values) {
int rowIndex = getRowIndex(cell);
if (row == null) {
row = new ChecksumRow(entry.getName());
ChecksumRow row;
if (rowIndex >= 0) {
// get existing row
row = rows.get(rowIndex);
} else {
// add new row
row = new ChecksumRow(cell.getName());
row.addPropertyChangeListener(stateListener);
rows.add(row);
}
row.put(entry);
// add cell to row
ChecksumCell old = row.put(cell);
// dispose of old cell
if (old != null) {
old.dispose();
replacements.add(cell);
}
// listen to changes (progress, state)
entry.addPropertyChangeListener(progressListener);
cell.addPropertyChangeListener(progressListener);
if (!columns.contains(entry.getRoot())) {
columns.add(entry.getRoot());
fireTableStructureChanged();
if (!checksumColumns.contains(cell.getRoot())) {
checksumColumns.add(cell.getRoot());
}
}
int lastRow = getRowCount() - 1;
// fire table events
if (columnCount != getColumnCount()) {
// number of columns has changed
fireTableStructureChanged();
return;
}
if (lastRow >= firstRow) {
fireTableRowsInserted(firstRow, lastRow);
for (ChecksumCell replacement : replacements) {
int row = getRowIndex(replacement);
// update this cell
fireTableCellUpdated(row, 0);
fireTableCellUpdated(row, getColumnIndex(replacement));
}
if (rowCount != getRowCount()) {
// some rows have been inserted
fireTableRowsInserted(rowCount, getRowCount() - 1);
}
}
@ -153,46 +235,40 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
public void clear() {
columns.clear();
checksumColumns.clear();
rows.clear();
fireTableStructureChanged();
}
private final PropertyChangeListener stateListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
int row = getRowIndex((ChecksumRow) evt.getSource());
if (row >= 0) {
// update only column 0 (state)
fireTableCellUpdated(row, 0);
}
}
};
private final PropertyChangeListener progressListener = new PropertyChangeListener() {
private final MutableTableModelEvent mutableUpdateEvent = new MutableTableModelEvent(ChecksumTableModel.this, UPDATE);
public void propertyChange(PropertyChangeEvent evt) {
ChecksumCell entry = (ChecksumCell) evt.getSource();
ChecksumCell cell = (ChecksumCell) evt.getSource();
int index = rows.getIndexByKey(entry.getName());
int row = getRowIndex(cell);
int column = getColumnIndex(cell);
if (index >= 0) {
rows.get(index).updateState();
fireTableChanged(mutableUpdateEvent.setRow(index));
if (row >= 0 && column >= 0) {
fireTableCellUpdated(row, column);
}
}
};
protected static class MutableTableModelEvent extends TableModelEvent {
public MutableTableModelEvent(TableModel source, int type) {
super(source, 0, 0, ALL_COLUMNS, type);
}
public MutableTableModelEvent setRow(int row) {
this.firstRow = row;
this.lastRow = row;
return this;
}
}
protected static abstract class IndexedMap<K, V> extends AbstractList<V> implements Set<V> {
private final Map<K, Integer> indexMap = new HashMap<K, Integer>(64);
@ -208,16 +284,6 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
}
public V getByKey(K key) {
Integer index = indexMap.get(key);
if (index == null)
return null;
return get(index);
}
public int getIndexByKey(K key) {
Integer index = indexMap.get(key);
@ -277,4 +343,16 @@ class ChecksumTableModel extends AbstractTableModel implements Iterable<Checksum
}
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
}

View File

@ -0,0 +1,198 @@
package net.sourceforge.filebot.ui.panel.sfv;
import static net.sourceforge.tuned.FileUtilities.containsOnly;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScanner.IllegalSyntaxException;
import net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy;
import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter;
class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<ChecksumCell> {
private final ChecksumTableModel model;
private final ChecksumComputationService computationService;
public ChecksumTableTransferablePolicy(ChecksumTableModel model, ChecksumComputationService checksumComputationService) {
this.model = model;
this.computationService = checksumComputationService;
}
@Override
protected boolean accept(List<File> files) {
return true;
}
@Override
protected void clear() {
super.clear();
computationService.reset();
model.clear();
}
@Override
protected void prepare(List<File> files) {
HashType type = getVerificationType(files);
if (type != null) {
model.setHashType(type);
}
}
@Override
protected void process(List<ChecksumCell> chunks) {
model.addAll(chunks);
}
@Override
protected void process(Exception e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
protected HashType getVerificationType(List<File> files) {
for (HashType hash : HashType.values()) {
if (containsOnly(files, new ExtensionFileFilter(hash.getExtension()))) {
return hash;
}
}
return null;
}
protected void loadVerificationFile(File file, HashType type) throws IOException {
// don't use new Scanner(File) because of BUG 6368019 (http://bugs.sun.com/view_bug.do?bug_id=6368019)
VerificationFileScanner scanner = type.newScanner(new Scanner(new FileInputStream(file), "UTF-8"));
try {
// root for relative file paths in verification file
File root = file.getParentFile();
while (scanner.hasNext()) {
try {
Entry<File, String> entry = scanner.next();
String name = normalizeRelativePath(entry.getKey());
String hash = entry.getValue();
ChecksumCell correct = new ChecksumCell(name, file, Collections.singletonMap(type, hash));
ChecksumCell current = createComputationCell(name, root, type);
publish(correct, current);
if (Thread.interrupted()) {
break;
}
} catch (IllegalSyntaxException e) {
// tell user about illegal lines in verification file
publish(e);
}
}
} finally {
scanner.close();
}
}
private final ThreadLocal<ExecutorService> executor = new ThreadLocal<ExecutorService>();
@Override
protected void load(List<File> files) throws IOException {
// initialize drop parameters
executor.set(computationService.newExecutor());
try {
HashType verificationType = getVerificationType(files);
if (verificationType != null) {
for (File file : files) {
loadVerificationFile(file, verificationType);
}
} else if ((files.size() == 1) && files.get(0).isDirectory()) {
// one single folder
File file = files.get(0);
for (File f : file.listFiles()) {
load(f, null, file);
}
} else {
// bunch of files
for (File f : files) {
load(f, null, f.getParentFile());
}
}
} catch (InterruptedException e) {
// supposed to happen if background execution was aborted
} finally {
// shutdown executor after all tasks have been completed
executor.get().shutdown();
// remove drop parameters
executor.remove();
}
}
protected void load(File file, File relativeFile, File root) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// add next name to relative path
relativeFile = new File(relativeFile, file.getName());
if (file.isDirectory()) {
// load all files in the file tree
for (File child : file.listFiles()) {
load(child, relativeFile, root);
}
} else {
publish(createComputationCell(normalizeRelativePath(relativeFile), root, model.getHashType()));
}
}
protected ChecksumCell createComputationCell(String name, File root, HashType hash) {
ChecksumCell cell = new ChecksumCell(name, root, new ChecksumComputationTask(new File(root, name), hash));
// start computation task
executor.get().execute(cell.getTask());
return cell;
}
protected String normalizeRelativePath(File file) {
if (file.isAbsolute())
throw new IllegalArgumentException("Path must be relative");
return file.getPath().replace('\\', '/');
}
@Override
public String getFileFilterDescription() {
return "files, folders and sfv files";
}
}

View File

@ -2,33 +2,106 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.io.File;
import java.util.Formatter;
import java.util.Scanner;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
enum HashType {
CRC32 {
SFV {
@Override
public Hash newInstance() {
public Hash newHash() {
return new ChecksumHash(new CRC32());
}
@Override
public VerificationFileScanner newScanner(Scanner scanner) {
// adapt default scanner to sfv line syntax
return new VerificationFileScanner(scanner) {
/**
* Pattern used to parse the lines of a sfv file.
*
* <pre>
* Sample:
* folder/file.txt 970E4EF1
* | Group 1 | | Gr.2 |
* </pre>
*/
private final Pattern pattern = Pattern.compile("(.+)\\s+(\\p{XDigit}{8})");
@Override
protected Entry<File, String> parseLine(String line) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches())
throw new IllegalSyntaxException(getLineNumber(), line);
return entry(new File(matcher.group(1)), matcher.group(2));
}
};
}
@Override
public VerificationFilePrinter newPrinter(Formatter out) {
return new VerificationFilePrinter(out, "CRC32") {
@Override
public void print(String path, String hash) {
// e.g folder/file.txt 970E4EF1
out.format(String.format("%s %s", path, hash));
}
};
}
},
MD5 {
@Override
public Hash newInstance() {
public Hash newHash() {
return new MessageDigestHash("MD5");
}
},
SHA1 {
@Override
public Hash newInstance() {
public Hash newHash() {
return new MessageDigestHash("SHA-1");
}
@Override
public String toString() {
return "SHA-1";
}
};
public abstract Hash newInstance();
public abstract Hash newHash();
public VerificationFileScanner newScanner(Scanner scanner) {
return new VerificationFileScanner(scanner);
}
public VerificationFilePrinter newPrinter(Formatter out) {
return new VerificationFilePrinter(out, this.name());
}
public String getExtension() {
return name().toLowerCase();
}
}

View File

@ -39,7 +39,7 @@ class HighlightPatternCellRenderer extends DefaultTableCellRenderer {
StringBuffer htmlText = new StringBuffer("<html><nobr>");
while (matcher.find()) {
matcher.appendReplacement(htmlText, "<span style='font-size: " + cssFontSize + ";" + (!isSelected ? "color: " + cssColor + ";" : "") + "'>$0</span>");
matcher.appendReplacement(htmlText, createReplacement(isSelected));
}
matcher.appendTail(htmlText);
@ -50,4 +50,21 @@ class HighlightPatternCellRenderer extends DefaultTableCellRenderer {
return this;
}
protected String createReplacement(boolean isSelected) {
// build replacement string like
// e.g. <span style='font-size: smaller; color: #009900;'>$0</span>
StringBuilder replacement = new StringBuilder(60);
replacement.append("<span style='");
replacement.append("font-size:").append(cssFontSize).append(';');
if (!isSelected) {
replacement.append("color:").append(cssColor).append(';');
}
return replacement.append("'>$0</span>").toString();
}
}

View File

@ -1,182 +0,0 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.ui.FileBotPanel;
import net.sourceforge.filebot.ui.FileTransferableMessageHandler;
import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler;
import net.sourceforge.filebot.ui.transfer.LoadAction;
import net.sourceforge.filebot.ui.transfer.SaveAction;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.MessageHandler;
import net.sourceforge.tuned.ui.TunedUtilities;
public class SfvPanel extends FileBotPanel {
private final ChecksumComputationService computationService = new ChecksumComputationService();
private final SfvTable table = new SfvTable();
private final SfvTransferablePolicy transferablePolicy = new SfvTransferablePolicy(table.getModel(), computationService);
private final ChecksumTableExportHandler exportHandler = new ChecksumTableExportHandler(table.getModel());
private final MessageHandler messageHandler = new FileTransferableMessageHandler(this, transferablePolicy);
public SfvPanel() {
super("SFV", ResourceManager.getIcon("panel.sfv"));
table.setTransferHandler(new DefaultTransferHandler(transferablePolicy, exportHandler));
table.setDragEnabled(true);
JPanel contentPane = new JPanel(new MigLayout("insets 0, nogrid, fill", null, "align bottom"));
contentPane.setBorder(new TitledBorder(getPanelName()));
setLayout(new MigLayout("insets dialog, fill"));
add(contentPane, "grow");
contentPane.add(new JScrollPane(table), "grow, wrap 10px");
contentPane.add(new JButton(loadAction), "gap 15px, gap bottom 4px");
contentPane.add(new JButton(saveAction), "gap rel, gap bottom 4px");
contentPane.add(new JButton(clearAction), "gap rel, gap bottom 4px");
contentPane.add(new TotalProgressPanel(computationService), "gap left indent:push, gap bottom 2px, gap right 7px, hidemode 3");
// Shortcut DELETE
TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction);
}
@Override
public MessageHandler getMessageHandler() {
return messageHandler;
}
private final SaveAction saveAction = new ChecksumTableSaveAction();
private final LoadAction loadAction = new LoadAction(transferablePolicy);
private final AbstractAction clearAction = new AbstractAction("Clear", ResourceManager.getIcon("action.clear")) {
public void actionPerformed(ActionEvent e) {
transferablePolicy.reset();
computationService.reset();
table.getModel().clear();
}
};
private final AbstractAction removeAction = new AbstractAction("Remove") {
public void actionPerformed(ActionEvent e) {
if (table.getSelectedRowCount() < 1)
return;
int firstSelectedRow = table.getSelectedRow();
// remove selected rows
table.getModel().remove(table.getSelectedRows());
// update computation service task count
computationService.purge();
// auto select next row
firstSelectedRow = Math.min(firstSelectedRow, table.getRowCount() - 1);
table.getSelectionModel().setSelectionInterval(firstSelectedRow, firstSelectedRow);
}
};
protected class ChecksumTableSaveAction extends SaveAction {
private File selectedColumn = null;
public ChecksumTableSaveAction() {
super(exportHandler);
}
@Override
public ChecksumTableExportHandler getExportHandler() {
return (ChecksumTableExportHandler) super.getExportHandler();
}
@Override
protected boolean canExport() {
return selectedColumn != null && super.canExport();
}
@Override
protected void export(File file) throws IOException {
getExportHandler().export(file, selectedColumn);
}
@Override
protected String getDefaultFileName() {
return getExportHandler().getDefaultFileName(selectedColumn);
}
@Override
protected File getDefaultFolder() {
// if the column is a folder use it as default folder in the file dialog
return selectedColumn.isDirectory() ? selectedColumn : null;
}
@Override
public void actionPerformed(ActionEvent e) {
List<File> options = table.getModel().getChecksumColumns();
this.selectedColumn = null;
if (options.size() == 1) {
// auto-select option if there is only one option
this.selectedColumn = options.get(0);
} else if (options.size() > 1) {
// user must select one option
SelectDialog<File> selectDialog = new SelectDialog<File>(SwingUtilities.getWindowAncestor(SfvPanel.this), options) {
@Override
protected String convertValueToString(Object value) {
return FileUtilities.getFolderName((File) value);
}
};
selectDialog.getHeaderLabel().setText("Select checksum column:");
selectDialog.setVisible(true);
this.selectedColumn = selectDialog.getSelectedValue();
}
if (this.selectedColumn != null) {
// continue if a column was selected
super.actionPerformed(e);
}
}
}
}

View File

@ -1,168 +0,0 @@
package net.sourceforge.filebot.ui.panel.sfv;
import static net.sourceforge.filebot.FileBotUtilities.SFV_FILES;
import static net.sourceforge.tuned.FileUtilities.containsOnly;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy;
import net.sourceforge.tuned.ExceptionUtilities;
class SfvTransferablePolicy extends BackgroundFileTransferablePolicy<ChecksumCell> {
private final ChecksumTableModel model;
private final ChecksumComputationService checksumComputationService;
public SfvTransferablePolicy(ChecksumTableModel model, ChecksumComputationService checksumComputationService) {
this.model = model;
this.checksumComputationService = checksumComputationService;
}
@Override
protected boolean accept(List<File> files) {
return true;
}
@Override
protected void clear() {
checksumComputationService.reset();
model.clear();
}
@Override
protected void process(List<ChecksumCell> chunks) {
model.addAll(chunks);
}
@Override
protected void process(Exception e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
protected void loadSfvFile(File sfvFile, Executor executor) {
try {
// don't use new Scanner(File) because of BUG 6368019 (http://bugs.sun.com/view_bug.do?bug_id=6368019)
Scanner scanner = new Scanner(new FileInputStream(sfvFile), "utf-8");
try {
Pattern pattern = Pattern.compile("(.+)\\s+(\\p{XDigit}{8})");
// root for relative file paths in sfv file
File root = sfvFile.getParentFile();
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (line.startsWith(";"))
continue;
Matcher matcher = pattern.matcher(line);
if (!matcher.matches())
continue;
String name = matcher.group(1);
String checksum = matcher.group(2);
ChecksumCell correct = new ChecksumCell(name, sfvFile, Collections.singletonMap(HashType.CRC32, checksum));
ChecksumCell current = createChecksumCell(name, root, new File(root, name), executor);
publish(correct, current);
if (Thread.interrupted()) {
break;
}
}
} finally {
scanner.close();
}
} catch (IOException e) {
// should not happen
Logger.getLogger("global").log(Level.SEVERE, e.toString(), e);
}
}
@Override
public String getFileFilterDescription() {
return "files, folders and sfv files";
}
@Override
protected void load(List<File> files) {
ExecutorService executor = checksumComputationService.newExecutor();
try {
if (containsOnly(files, SFV_FILES)) {
// one or more sfv files
for (File file : files) {
loadSfvFile(file, executor);
}
} else if ((files.size() == 1) && files.get(0).isDirectory()) {
// one single folder
File file = files.get(0);
for (File f : file.listFiles()) {
load(f, file, "", executor);
}
} else {
// bunch of files
for (File f : files) {
load(f, f.getParentFile(), "", executor);
}
}
} catch (InterruptedException e) {
// supposed to happen if background execution was aborted
} finally {
executor.shutdown();
}
}
protected void load(File file, File root, String prefix, Executor executor) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (file.isDirectory()) {
// load all files in the file tree
String newPrefix = prefix + file.getName() + "/";
for (File f : file.listFiles()) {
load(f, root, newPrefix, executor);
}
} else if (file.isFile()) {
publish(createChecksumCell(prefix + file.getName(), root, file, executor));
}
}
protected ChecksumCell createChecksumCell(String name, File root, File file, Executor executor) {
ChecksumCell cell = new ChecksumCell(name, root, new ChecksumComputationTask(new File(root, name), HashType.CRC32));
// start computation task
executor.execute(cell.getTask());
return cell;
}
}

View File

@ -15,12 +15,12 @@ import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.ui.panel.sfv.ChecksumRow.State;
class StateIconTableCellRenderer extends DefaultTableCellRenderer {
class StateIconCellRenderer extends DefaultTableCellRenderer {
private final Map<State, Icon> icons = new EnumMap<State, Icon>(State.class);
public StateIconTableCellRenderer() {
public StateIconCellRenderer() {
icons.put(State.UNKNOWN, ResourceManager.getIcon("status.unknown"));
icons.put(State.OK, ResourceManager.getIcon("status.ok"));
icons.put(State.WARNING, ResourceManager.getIcon("status.warning"));

View File

@ -0,0 +1,74 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Rectangle;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.border.EmptyBorder;
import javax.swing.table.TableCellRenderer;
class SwingWorkerCellRenderer extends JPanel implements TableCellRenderer {
private final JProgressBar progressBar = new JProgressBar(0, 100);
public SwingWorkerCellRenderer() {
super(new BorderLayout());
// create margin for progress bar,
// because setting margin for progress bar directly does not work (border size is not respected in the paint method)
setBorder(new EmptyBorder(2, 2, 2, 2));
progressBar.setStringPainted(true);
add(progressBar, BorderLayout.CENTER);
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
progressBar.setValue(((SwingWorker<?, ?>) value).getProgress());
return this;
}
/**
* Overridden for performance reasons.
*/
@Override
public void repaint(long tm, int x, int y, int width, int height) {
}
/**
* Overridden for performance reasons.
*/
@Override
public void repaint(Rectangle r) {
}
/**
* Overridden for performance reasons.
*/
@Override
public void repaint() {
}
/**
* Overridden for performance reasons.
*/
@Override
public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
}
}

View File

@ -4,41 +4,36 @@ package net.sourceforge.filebot.ui.panel.sfv;
import static net.sourceforge.filebot.ui.panel.sfv.ChecksumComputationService.TASK_COUNT_PROPERTY;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.TitledBorder;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.tuned.ui.TunedUtilities;
class TotalProgressPanel extends JComponent {
private int millisToSetVisible = 200;
private final JProgressBar progressBar = new JProgressBar(0, 0);
private final ChecksumComputationService computationService;
private final int millisToSetVisible = 200;
public TotalProgressPanel(ChecksumComputationService computationService) {
this.computationService = computationService;
setLayout(new MigLayout("insets 1px"));
setLayout(new MigLayout());
setBorder(new TitledBorder("Total Progress"));
// invisible by default
setVisible(false);
progressBar.setStringPainted(true);
progressBar.setBorderPainted(false);
progressBar.setString("");
setBorder(BorderFactory.createTitledBorder("Total Progress"));
add(progressBar, "growx");
@ -47,45 +42,82 @@ class TotalProgressPanel extends JComponent {
private final PropertyChangeListener progressListener = new PropertyChangeListener() {
private Timer setVisibleTimer;
private static final String SHOW = "show";
private static final String HIDE = "hide";
private final DelayedToggle delayed = new DelayedToggle();
public void propertyChange(PropertyChangeEvent evt) {
final int completedTaskCount = computationService.getCompletedTaskCount();
final int totalTaskCount = computationService.getTotalTaskCount();
final int completedTaskCount = getComputationService(evt).getCompletedTaskCount();
final int totalTaskCount = getComputationService(evt).getTotalTaskCount();
// invoke on EDT
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (completedTaskCount < totalTaskCount) {
if (setVisibleTimer == null) {
setVisibleTimer = TunedUtilities.invokeLater(millisToSetVisible, new Runnable() {
@Override
public void run() {
setVisible(computationService.getTaskCount() > computationService.getCompletedTaskCount());
}
});
}
} else {
if (setVisibleTimer != null) {
setVisibleTimer.stop();
setVisibleTimer = null;
}
// hide when not active
setVisible(false);
if (completedTaskCount == totalTaskCount) {
// delayed hide on reset, immediate hide on finish
delayed.toggle(HIDE, totalTaskCount == 0 ? millisToSetVisible : 0, visibilityActionHandler);
} else if (totalTaskCount != 0) {
delayed.toggle(SHOW, millisToSetVisible, visibilityActionHandler);
}
progressBar.setValue(completedTaskCount);
progressBar.setMaximum(totalTaskCount);
progressBar.setString(String.format("%d / %d", completedTaskCount, totalTaskCount));
if (totalTaskCount != 0) {
progressBar.setValue(completedTaskCount);
progressBar.setMaximum(totalTaskCount);
progressBar.setString(String.format("%d / %d", completedTaskCount, totalTaskCount));
}
};
});
}
private ChecksumComputationService getComputationService(PropertyChangeEvent evt) {
return ((ChecksumComputationService) evt.getSource());
}
private final ActionListener visibilityActionHandler = new ActionListener() {
public void actionPerformed(ActionEvent e) {
setVisible(e.getActionCommand() == SHOW);
}
};
};
protected static class DelayedToggle {
private Timer timer = null;
public void toggle(String action, int delay, final ActionListener actionHandler) {
if (timer != null) {
if (action.equals(timer.getActionCommand())) {
// action has not changed, don't stop existing timer
return;
}
timer.stop();
}
timer = new Timer(delay, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
actionHandler.actionPerformed(e);
}
});
timer.setActionCommand(action);
timer.setRepeats(false);
timer.start();
}
}
}

View File

@ -0,0 +1,42 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.io.Closeable;
import java.io.IOException;
import java.util.Formatter;
class VerificationFilePrinter implements Closeable {
protected final Formatter out;
protected final String algorithm;
public VerificationFilePrinter(Formatter out, String algorithm) {
this.out = out;
this.algorithm = algorithm;
}
public void println(String path, String hash) {
// print entry
print(path, hash);
// print line separator
out.format("%n");
}
protected void print(String path, String hash) {
// e.g. 1a02a7c1e9ac91346d08829d5037b240f42ded07 ?SHA1*folder/file.txt
out.format("%s ?%s*%s", hash, algorithm.toUpperCase(), path);
}
@Override
public void close() throws IOException {
out.close();
}
}

View File

@ -0,0 +1,144 @@
package net.sourceforge.filebot.ui.panel.sfv;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeable {
private final Scanner scanner;
private String cache;
private int lineNumber = 0;
public VerificationFileScanner(File file) throws FileNotFoundException {
// don't use new Scanner(File) because of BUG 6368019 (http://bugs.sun.com/view_bug.do?bug_id=6368019)
this(new Scanner(new FileInputStream(file), "UTF-8"));
}
public VerificationFileScanner(Scanner scanner) {
this.scanner = scanner;
}
@Override
public boolean hasNext() {
if (cache == null) {
// cache next line
cache = nextLine();
}
return cache != null;
}
@Override
public Entry<File, String> next() {
// cache next line
if (!hasNext()) {
throw new NoSuchElementException();
}
try {
return parseLine(cache);
} finally {
// invalidate cache
cache = null;
}
}
protected String nextLine() {
String line = null;
// get next non-comment line
while (scanner.hasNext() && (line == null || isComment(line))) {
line = scanner.nextLine().trim();
lineNumber++;
}
return line;
}
/**
* Pattern used to parse the lines of a md5 or sha1 file.
*
* <pre>
* Sample MD5:
* 50e85fe18e17e3616774637a82968f4c *folder/file.txt
* | Group 1 | Group 2 |
*
* Sample SHA-1:
* 1a02a7c1e9ac91346d08829d5037b240f42ded07 ?SHA1*folder/file.txt
* | Group 1 | | Group 2 |
* </pre>
*/
private final Pattern pattern = Pattern.compile("(\\p{XDigit}{8,})\\s+(?:\\?\\w+)?\\*(.+)");
protected Entry<File, String> parseLine(String line) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches())
throw new IllegalSyntaxException(getLineNumber(), line);
return entry(new File(matcher.group(2)), matcher.group(1));
}
public int getLineNumber() {
return lineNumber;
}
protected boolean isComment(String line) {
return line.startsWith(";") || line.isEmpty();
}
protected Entry<File, String> entry(File file, String hash) {
return new SimpleEntry<File, String>(file, hash);
}
@Override
public void close() throws IOException {
scanner.close();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
public static class IllegalSyntaxException extends RuntimeException {
public IllegalSyntaxException(int lineNumber, String line) {
this(String.format("Illegal syntax in line %d: %s", lineNumber, line));
}
public IllegalSyntaxException(String message) {
super(message);
}
}
}

View File

@ -3,44 +3,46 @@ package net.sourceforge.filebot.ui.transfer;
import java.awt.datatransfer.Transferable;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.SwingPropertyChangeSupport;
public abstract class BackgroundFileTransferablePolicy<V> extends FileTransferablePolicy {
public static final String LOADING_PROPERTY = "loading";
private final ThreadLocal<BackgroundWorker> threadLocalWorker = new ThreadLocal<BackgroundWorker>() {
@Override
protected BackgroundWorker initialValue() {
// fail if a non-background-worker thread is trying to access the thread-local worker object
throw new IllegalThreadStateException("Illegal access thread");
}
};
private final ThreadLocal<BackgroundWorker> threadLocalWorker = new ThreadLocal<BackgroundWorker>();
private final List<BackgroundWorker> workers = new ArrayList<BackgroundWorker>(2);
@Override
public void handleTransferable(Transferable tr, TransferAction action) {
public void handleTransferable(Transferable tr, TransferAction action) throws Exception {
List<File> files = getFilesFromTransferable(tr);
if (action != TransferAction.ADD)
if (action != TransferAction.ADD) {
clear();
}
prepare(files);
// create and start worker
new BackgroundWorker(files).execute();
}
protected void prepare(List<File> files) {
}
@Override
protected void clear() {
// stop other workers on clear (before starting new worker)
@ -61,14 +63,39 @@ public abstract class BackgroundFileTransferablePolicy<V> extends FileTransferab
}
public boolean isLoading() {
synchronized (workers) {
return !workers.isEmpty();
}
}
protected abstract void process(List<V> chunks);
protected abstract void process(Exception e);
protected abstract void process(Exception exception);
protected final void publish(V... chunks) {
threadLocalWorker.get().offer(chunks);
BackgroundWorker worker = threadLocalWorker.get();
if (worker == null) {
// fail if a non-background-worker thread is trying to access the thread-local worker object
throw new IllegalThreadStateException("Illegal access thread");
}
worker.offer(chunks);
}
protected final void publish(final Exception exception) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
process(exception);
}
});
}
@ -90,7 +117,7 @@ public abstract class BackgroundFileTransferablePolicy<V> extends FileTransferab
@Override
protected Object doInBackground() {
protected Object doInBackground() throws Exception {
// associate this worker with the current (background) thread
threadLocalWorker.set(this);
@ -121,13 +148,6 @@ public abstract class BackgroundFileTransferablePolicy<V> extends FileTransferab
@Override
protected void done() {
// unregister worker
synchronized (workers) {
if (workers.remove(this) && workers.isEmpty()) {
swingPropertyChangeSupport.firePropertyChange(LOADING_PROPERTY, true, false);
}
}
if (!isCancelled()) {
try {
// check for exception
@ -136,27 +156,17 @@ public abstract class BackgroundFileTransferablePolicy<V> extends FileTransferab
BackgroundFileTransferablePolicy.this.process(e);
}
}
// unregister worker
synchronized (workers) {
if (workers.remove(this) && workers.isEmpty()) {
swingPropertyChangeSupport.firePropertyChange(LOADING_PROPERTY, true, false);
}
}
}
}
protected final PropertyChangeSupport swingPropertyChangeSupport = new PropertyChangeSupport(this) {
@Override
public void firePropertyChange(final PropertyChangeEvent evt) {
if (SwingUtilities.isEventDispatchThread()) {
super.firePropertyChange(evt);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
swingPropertyChangeSupport.firePropertyChange(evt);
}
});
}
}
};
protected final PropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(this, true);
public void addPropertyChangeListener(PropertyChangeListener listener) {

View File

@ -4,9 +4,6 @@ package net.sourceforge.filebot.ui.transfer;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JTable;
@ -16,56 +13,58 @@ import javax.swing.tree.TreePath;
public class DefaultClipboardHandler implements ClipboardHandler {
protected final String newLine = System.getProperty("line.separator");
@Override
public void exportToClipboard(JComponent component, Clipboard clip, int action) throws IllegalStateException {
StringWriter buffer = new StringWriter();
StringBuilder sb = new StringBuilder();
if (component instanceof JList) {
export(new PrintWriter(buffer), (JList) component);
export(sb, (JList) component);
} else if (component instanceof JTree) {
export(new PrintWriter(buffer), (JTree) component);
export(sb, (JTree) component);
} else if (component instanceof JTable) {
export(new PrintWriter(buffer), (JTable) component);
export(sb, (JTable) component);
}
clip.setContents(new StringSelection(buffer.toString()), null);
clip.setContents(new StringSelection(sb.toString()), null);
}
protected void export(PrintWriter out, JList list) {
protected void export(StringBuilder sb, JList list) {
for (Object value : list.getSelectedValues()) {
out.println(valueToString(value));
sb.append(value == null ? "" : value).append(newLine);
}
}
protected void export(PrintWriter out, JTree tree) {
protected void export(StringBuilder sb, JTree tree) {
for (TreePath path : tree.getSelectionPaths()) {
out.println(valueToString(path.getLastPathComponent()));
Object value = path.getLastPathComponent();
sb.append(value == null ? "" : value).append(newLine);
}
}
protected void export(PrintWriter out, JTable table) {
protected void export(StringBuilder sb, JTable table) {
for (int row : table.getSelectedRows()) {
for (int columnIndex = 0; columnIndex < table.getColumnCount(); columnIndex++) {
out.print(valueToString(table.getModel().getValueAt(row, columnIndex)));
int modelRow = table.getRowSorter().convertRowIndexToModel(row);
for (int column = 0; column < table.getColumnCount(); column++) {
Object value = table.getModel().getValueAt(modelRow, column);
if (columnIndex < table.getColumnCount() - 1)
out.print("\t");
if (value != null) {
sb.append(value);
}
if (column < table.getColumnCount() - 1) {
sb.append("\t");
}
}
out.println();
sb.append(newLine);
}
}
protected String valueToString(Object value) {
// return empty string for null values
if (value == null)
return "";
return value.toString();
}
}

View File

@ -4,7 +4,6 @@ package net.sourceforge.filebot.ui.transfer;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -27,7 +26,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
@Override
public boolean accept(Transferable tr) {
public boolean accept(Transferable tr) throws Exception {
List<File> files = getFilesFromTransferable(tr);
if (files.isEmpty())
@ -38,48 +37,40 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
@SuppressWarnings("unchecked")
protected List<File> getFilesFromTransferable(Transferable tr) {
try {
if (tr.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
// file list flavor
return (List<File>) tr.getTransferData(DataFlavor.javaFileListFlavor);
} else if (tr.isDataFlavorSupported(FileTransferable.uriListFlavor)) {
// file URI list flavor
String transferData = (String) tr.getTransferData(FileTransferable.uriListFlavor);
protected List<File> getFilesFromTransferable(Transferable tr) throws Exception {
if (tr.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
// file list flavor
return (List<File>) tr.getTransferData(DataFlavor.javaFileListFlavor);
} else if (tr.isDataFlavorSupported(FileTransferable.uriListFlavor)) {
// file URI list flavor
String transferData = (String) tr.getTransferData(FileTransferable.uriListFlavor);
Scanner scanner = new Scanner(transferData).useDelimiter(LINE_SEPARATOR);
List<File> files = new ArrayList<File>();
while (scanner.hasNext()) {
String uri = scanner.next();
Scanner scanner = new Scanner(transferData).useDelimiter(LINE_SEPARATOR);
ArrayList<File> files = new ArrayList<File>();
while (scanner.hasNext()) {
String uri = scanner.next();
if (uri.startsWith("#")) {
// the line is a comment (as per RFC 2483)
continue;
}
try {
File file = new File(new URI(uri));
if (!file.exists())
throw new FileNotFoundException(file.toString());
files.add(file);
} catch (Exception e) {
// URISyntaxException, IllegalArgumentException, FileNotFoundException
Logger.getLogger("global").log(Level.WARNING, "Invalid file uri: " + uri);
}
if (uri.startsWith("#")) {
// the line is a comment (as per RFC 2483)
continue;
}
return files;
try {
File file = new File(new URI(uri));
if (!file.exists())
throw new FileNotFoundException(file.toString());
files.add(file);
} catch (Exception e) {
// URISyntaxException, IllegalArgumentException, FileNotFoundException
Logger.getLogger("global").log(Level.WARNING, "Invalid file uri: " + uri);
}
}
} catch (UnsupportedFlavorException e) {
// should not happen
throw new RuntimeException(e);
} catch (IOException e) {
// should not happen
throw new RuntimeException(e);
return files;
}
return Collections.emptyList();
@ -87,7 +78,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
@Override
public void handleTransferable(Transferable tr, TransferAction action) {
public void handleTransferable(Transferable tr, TransferAction action) throws Exception {
List<File> files = getFilesFromTransferable(tr);
if (action == TransferAction.PUT) {
@ -101,7 +92,7 @@ public abstract class FileTransferablePolicy extends TransferablePolicy {
protected abstract boolean accept(List<File> files);
protected abstract void load(List<File> files);
protected abstract void load(List<File> files) throws IOException;
protected abstract void clear();

View File

@ -3,6 +3,8 @@ package net.sourceforge.filebot.ui.transfer;
import java.awt.event.ActionEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.JFileChooser;
@ -23,7 +25,7 @@ public class LoadAction extends AbstractAction {
}
public void actionPerformed(ActionEvent e) {
public void actionPerformed(ActionEvent evt) {
// get transferable policy from action properties
TransferablePolicy transferablePolicy = (TransferablePolicy) getValue(TRANSFERABLE_POLICY);
@ -45,10 +47,17 @@ public class LoadAction extends AbstractAction {
TransferAction action = TransferAction.PUT;
// if CTRL was pressed when the button was clicked, assume ADD action (same as with dnd)
if ((e.getModifiers() & ActionEvent.CTRL_MASK) != 0)
if ((evt.getModifiers() & ActionEvent.CTRL_MASK) != 0) {
action = TransferAction.ADD;
}
if (transferablePolicy.accept(transferable))
transferablePolicy.handleTransferable(transferable, action);
try {
if (transferablePolicy.accept(transferable)) {
transferablePolicy.handleTransferable(transferable, action);
}
} catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e);
}
}
}

View File

@ -13,14 +13,13 @@ import javax.swing.TransferHandler.TransferSupport;
public abstract class TransferablePolicy {
public abstract boolean accept(Transferable tr);
public abstract boolean accept(Transferable tr) throws Exception;
public abstract void handleTransferable(Transferable tr, TransferAction action);
public abstract void handleTransferable(Transferable tr, TransferAction action) throws Exception;
public boolean canImport(TransferSupport support) {
if (support.isDrop())
support.setShowDropLocation(false);
@ -32,6 +31,8 @@ public abstract class TransferablePolicy {
// just assume that the transferable will be accepted, accept will be called in importData again anyway
return true;
} catch (Exception e) {
return false;
}
}

View File

@ -7,7 +7,6 @@ import java.io.File;
import javax.swing.filechooser.FileFilter;
public class TransferablePolicyFileFilter extends FileFilter {
private final TransferablePolicy transferablePolicy;
@ -23,7 +22,11 @@ public class TransferablePolicyFileFilter extends FileFilter {
if (f.isDirectory())
return true;
return transferablePolicy.accept(new FileTransferable(f));
try {
return transferablePolicy.accept(new FileTransferable(f));
} catch (Exception e) {
return false;
}
}

View File

@ -36,8 +36,11 @@ public class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread thread = new Thread(group, r, String.format("%s-thread-%d", group.getName(), threadNumber.incrementAndGet()));
thread.setDaemon(daemon);
thread.setPriority(priority);
if (daemon != thread.isDaemon())
thread.setDaemon(daemon);
if (priority != thread.getPriority())
thread.setPriority(priority);
return thread;
}

View File

@ -22,7 +22,7 @@ public final class ExceptionUtilities {
String message = t.getMessage();
if (message == null || message.isEmpty()) {
return t.toString();
message = t.toString().replaceAll(t.getClass().getName(), t.getClass().getSimpleName());
}
return message;

View File

@ -6,6 +6,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
@ -73,7 +75,11 @@ public class MessageBus {
private void publishDirect(String topic, Object... messages) {
for (MessageHandler handler : getHandlers(topic.toLowerCase())) {
handler.handle(topic.toLowerCase(), messages);
try {
handler.handle(topic.toLowerCase(), messages);
} catch (Exception e) {
Logger.getLogger("global").log(Level.SEVERE, e.getMessage(), e);
}
}
}

View File

@ -4,6 +4,6 @@ package net.sourceforge.tuned;
public interface MessageHandler {
public void handle(String topic, Object... messages);
public void handle(String topic, Object... messages) throws Exception;
}

View File

@ -67,6 +67,11 @@ public class ActionPopup extends JPopupMenu {
}
public void clear() {
actionPanel.removeAll();
}
@Override
public void setLabel(String label) {
headerLabel.setText(label);

View File

@ -93,7 +93,6 @@ public final class TunedUtilities {
public void actionPerformed(ActionEvent e) {
runnable.run();
}
});
timer.setRepeats(false);

View File

@ -11,7 +11,7 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { SimilarityTestSuite.class, WebTestSuite.class, ArgumentBeanTest.class })
@SuiteClasses( { SimilarityTestSuite.class, WebTestSuite.class, MiscSuite.class })
public class FileBotTestSuite {
}

View File

@ -0,0 +1,15 @@
package net.sourceforge.filebot;
import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScannerTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { ArgumentBeanTest.class, VerificationFileScannerTest.class })
public class MiscSuite {
}

View File

@ -0,0 +1,50 @@
package net.sourceforge.filebot.ui.panel.sfv;
import static org.junit.Assert.*;
import java.io.File;
import java.util.Scanner;
import java.util.Map.Entry;
import org.junit.Test;
public class VerificationFileScannerTest {
@Test
public void nextLine() {
// trim lines, skip empty lines
String text = String.format("%s %n %n%n%n %s%n%s", "line 1", "line 2", "line 3");
VerificationFileScanner lines = new VerificationFileScanner(new Scanner(text));
assertEquals("line 1", lines.nextLine());
assertEquals("line 2", lines.nextLine());
assertEquals("line 3", lines.nextLine());
assertFalse(lines.hasNext());
}
@Test
public void parseLine() {
VerificationFileScanner reader = new VerificationFileScanner(new Scanner("null"));
// md5
Entry<File, String> md5 = reader.parseLine("50e85fe18e17e3616774637a82968f4c *folder/file.txt");
assertEquals("file.txt", md5.getKey().getName());
assertEquals("folder", md5.getKey().getParent());
assertEquals("50e85fe18e17e3616774637a82968f4c", md5.getValue());
// sha1
Entry<File, String> sha1 = reader.parseLine("1a02a7c1e9ac91346d08829d5037b240f42ded07 ?SHA1*folder/file.txt");
assertEquals("file.txt", sha1.getKey().getName());
assertEquals("folder", sha1.getKey().getParent());
assertEquals("1a02a7c1e9ac91346d08829d5037b240f42ded07", sha1.getValue());
}
}