filebot/source/net/filebot/ui/rename/RenameAction.java

411 lines
13 KiB
Java
Raw Normal View History

package net.sourceforge.filebot.ui.rename;
2009-05-17 16:58:20 -04:00
import static java.util.Collections.*;
import static javax.swing.JOptionPane.*;
import static net.sourceforge.filebot.Settings.*;
import static net.sourceforge.filebot.ui.NotificationLogging.*;
import static net.sourceforge.filebot.util.ExceptionUtilities.*;
import static net.sourceforge.filebot.util.FileUtilities.*;
import static net.sourceforge.filebot.util.ui.TunedUtilities.*;
2009-05-17 16:58:20 -04:00
2011-11-04 03:45:48 -04:00
import java.awt.Cursor;
import java.awt.Dimension;
2009-05-17 16:58:20 -04:00
import java.awt.Window;
import java.awt.event.ActionEvent;
2011-11-04 03:45:48 -04:00
import java.beans.PropertyChangeEvent;
import java.io.File;
2009-05-17 16:58:20 -04:00
import java.util.AbstractList;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
2011-11-04 03:45:48 -04:00
import java.util.HashMap;
import java.util.HashSet;
2011-11-04 03:45:48 -04:00
import java.util.LinkedHashMap;
import java.util.List;
2009-05-17 16:58:20 -04:00
import java.util.Map;
import java.util.Map.Entry;
2011-11-04 03:45:48 -04:00
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
2011-11-04 03:45:48 -04:00
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
2011-11-04 03:45:48 -04:00
import javax.swing.Icon;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
2011-11-04 03:45:48 -04:00
import javax.swing.SwingWorker;
import net.sourceforge.filebot.Analytics;
2012-07-09 07:22:12 -04:00
import net.sourceforge.filebot.HistorySpooler;
import net.sourceforge.filebot.NativeRenameAction;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.StandardRenameAction;
import net.sourceforge.filebot.media.MediaDetection;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.util.ui.ProgressDialog;
import net.sourceforge.filebot.util.ui.ProgressDialog.Cancellable;
import net.sourceforge.filebot.util.ui.SwingWorkerPropertyChangeAdapter;
class RenameAction extends AbstractAction {
2013-09-30 00:46:33 -04:00
public static final String RENAME_ACTION = "RENAME_ACTION";
2013-09-30 00:46:33 -04:00
private final RenameModel model;
2013-09-30 00:46:33 -04:00
public RenameAction(RenameModel model) {
2009-05-17 16:58:20 -04:00
this.model = model;
resetValues();
}
2013-09-30 00:46:33 -04:00
public void resetValues() {
putValue(RENAME_ACTION, StandardRenameAction.MOVE);
2009-05-17 16:58:20 -04:00
putValue(NAME, "Rename");
putValue(SMALL_ICON, ResourceManager.getIcon("action.rename"));
}
2013-09-30 00:46:33 -04:00
@Override
public void actionPerformed(ActionEvent evt) {
Window window = getWindow(evt.getSource());
try {
2013-10-28 01:49:00 -04:00
if (model.files().isEmpty() || model.values().isEmpty()) {
UILogger.info("Nothing to rename. Please add some files and fetch naming data first.");
return;
}
2013-09-30 00:46:33 -04:00
Map<File, File> renameMap = checkRenamePlan(validate(model.getRenameMap(), window), window);
if (renameMap.isEmpty()) {
return;
}
2013-09-30 00:46:33 -04:00
// start processing
List<Match<Object, File>> matches = new ArrayList<Match<Object, File>>(model.matches());
StandardRenameAction action = (StandardRenameAction) getValue(RENAME_ACTION);
2013-09-30 00:46:33 -04:00
window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
2013-09-30 00:46:33 -04:00
if (useNativeShell() && isNativeActionSupported(action)) {
RenameJob renameJob = new NativeRenameJob(renameMap, NativeRenameAction.valueOf(action.name()));
renameJob.execute();
try {
renameJob.get(); // wait for native operation to finish or be cancelled
} catch (CancellationException e) {
// ignore
}
} else {
RenameJob renameJob = new RenameJob(renameMap, action);
renameJob.execute();
2013-09-30 00:46:33 -04:00
try {
// wait a for little while (renaming might finish in less than a second)
renameJob.get(2, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
// move/renaming will probably take a while
ProgressDialog dialog = createProgressDialog(window, renameJob);
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setIndeterminate(true);
2013-09-30 00:46:33 -04:00
// display progress dialog and stop blocking EDT
window.setCursor(Cursor.getDefaultCursor());
dialog.setVisible(true);
}
2009-05-17 16:58:20 -04:00
}
// write metadata into xattr if xattr is enabled
2014-01-25 22:51:47 -05:00
if (useExtendedFileAttributes() || useCreationDate()) {
try {
for (Match<Object, File> match : matches) {
File file = match.getCandidate();
Object meta = match.getValue();
if (renameMap.containsKey(file) && meta != null) {
File destination = resolveDestination(file, renameMap.get(file), false);
if (destination.isFile()) {
2014-01-25 22:51:47 -05:00
MediaDetection.storeMetaInfo(destination, meta, file.getName(), useExtendedFileAttributes(), useCreationDate());
}
}
}
} catch (Throwable e) {
Logger.getLogger(RenameAction.class.getName()).warning("Failed to write xattr: " + e.getMessage());
}
}
} catch (ExecutionException e) {
// ignore, handled in rename worker
} catch (Throwable e) {
2011-11-20 16:32:24 -05:00
UILogger.log(Level.WARNING, e.getMessage(), e);
2009-05-17 16:58:20 -04:00
}
2013-09-30 00:46:33 -04:00
window.setCursor(Cursor.getDefaultCursor());
2011-11-04 03:45:48 -04:00
}
2013-09-30 00:46:33 -04:00
public boolean isNativeActionSupported(StandardRenameAction action) {
try {
return NativeRenameAction.isSupported() && NativeRenameAction.valueOf(action.name()) != null;
} catch (IllegalArgumentException e) {
return false;
}
}
2013-09-30 00:46:33 -04:00
private Map<File, File> checkRenamePlan(List<Entry<File, File>> renamePlan, Window parent) {
2011-11-04 03:45:48 -04:00
// build rename map and perform some sanity checks
Map<File, File> renameMap = new HashMap<File, File>();
Set<File> destinationSet = new HashSet<File>();
List<String> issues = new ArrayList<String>();
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
for (Entry<File, File> mapping : renamePlan) {
File source = mapping.getKey();
File destination = mapping.getValue();
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// resolve destination
if (!destination.isAbsolute()) {
// same folder, different name
destination = new File(source.getParentFile(), destination.getPath());
}
2013-09-30 00:46:33 -04:00
try {
if (renameMap.containsKey(source))
throw new IllegalArgumentException("Duplicate source file: " + source.getPath());
2013-09-30 00:46:33 -04:00
if (destinationSet.contains(destination))
throw new IllegalArgumentException("Conflict detected: " + mapping.getValue().getPath());
2013-09-30 00:46:33 -04:00
if (destination.exists() && !resolveDestination(mapping.getKey(), mapping.getValue(), false).equals(mapping.getKey()))
throw new IllegalArgumentException("File already exists: " + mapping.getValue().getPath());
2013-09-30 00:46:33 -04:00
if (getExtension(destination) == null && destination.isFile())
throw new IllegalArgumentException("Missing extension: " + mapping.getValue().getPath());
// use original mapping values
renameMap.put(mapping.getKey(), mapping.getValue());
destinationSet.add(destination);
} catch (Exception e) {
issues.add(e.getMessage());
}
}
2013-09-30 00:46:33 -04:00
if (issues.size() > 0) {
String text = "We found some issues. Do you to continue?";
JList issuesComponent = new JList(issues.toArray()) {
2013-09-30 00:46:33 -04:00
@Override
public Dimension getPreferredScrollableViewportSize() {
// adjust component size
return new Dimension(80, 80);
}
};
Object[] message = new Object[] { text, new JScrollPane(issuesComponent) };
String[] actions = new String[] { "Continue", "Cancel" };
JOptionPane pane = new JOptionPane(message, PLAIN_MESSAGE, YES_NO_OPTION, null, actions, actions[1]);
2013-09-30 00:46:33 -04:00
// display option dialog
pane.createDialog(getWindow(parent), "Conflicting Files").setVisible(true);
2013-09-30 00:46:33 -04:00
if (pane.getValue() != actions[0]) {
return emptyMap();
}
2009-05-17 16:58:20 -04:00
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
return renameMap;
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
private List<Entry<File, File>> validate(Map<File, String> renameMap, Window parent) {
2011-10-10 12:21:54 -04:00
final List<Entry<File, File>> source = new ArrayList<Entry<File, File>>(renameMap.size());
2013-09-30 00:46:33 -04:00
2011-10-10 12:21:54 -04:00
for (Entry<File, String> entry : renameMap.entrySet()) {
source.add(new SimpleEntry<File, File>(entry.getKey(), new File(entry.getValue())));
}
2013-09-30 00:46:33 -04:00
2011-10-10 12:21:54 -04:00
List<File> destinationFileNameView = new AbstractList<File>() {
2013-09-30 00:46:33 -04:00
2009-05-17 16:58:20 -04:00
@Override
2011-10-10 12:21:54 -04:00
public File get(int index) {
return source.get(index).getValue();
2009-05-17 16:58:20 -04:00
}
2013-09-30 00:46:33 -04:00
2009-05-17 16:58:20 -04:00
@Override
2011-10-10 12:21:54 -04:00
public File set(int index, File name) {
return source.get(index).setValue(name);
2009-05-17 16:58:20 -04:00
}
2013-09-30 00:46:33 -04:00
2009-05-17 16:58:20 -04:00
@Override
public int size() {
return source.size();
}
};
2013-09-30 00:46:33 -04:00
2009-05-17 16:58:20 -04:00
if (ValidateDialog.validate(parent, destinationFileNameView)) {
// names have been validated via view
return source;
}
2013-09-30 00:46:33 -04:00
2009-05-17 16:58:20 -04:00
// return empty list if validation was cancelled
return emptyList();
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
protected ProgressDialog createProgressDialog(Window parent, final RenameJob job) {
final ProgressDialog dialog = new ProgressDialog(parent, job);
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// configure dialog
dialog.setTitle("Moving files...");
dialog.setIcon((Icon) getValue(SMALL_ICON));
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// close progress dialog when worker is finished
job.addPropertyChangeListener(new SwingWorkerPropertyChangeAdapter() {
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
@Override
protected void event(String name, Object oldValue, Object newValue) {
if (name.equals("currentFile")) {
int i = job.renameLog.size();
int n = job.renameMap.size();
dialog.setNote(String.format("%d of %d", i + 1, n));
2012-02-24 08:39:32 -05:00
if (newValue instanceof File) {
dialog.setWindowTitle("Moving " + ((File) oldValue).getName());
}
2011-11-04 03:45:48 -04:00
}
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
@Override
protected void done(PropertyChangeEvent evt) {
dialog.close();
}
});
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
return dialog;
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
protected class RenameJob extends SwingWorker<Map<File, File>, Void> implements Cancellable {
2013-09-30 00:46:33 -04:00
protected final net.sourceforge.filebot.RenameAction action;
2013-09-30 00:46:33 -04:00
protected final Map<File, File> renameMap;
protected final Map<File, File> renameLog;
2013-09-30 00:46:33 -04:00
protected final Semaphore postprocess = new Semaphore(0);
2013-09-30 00:46:33 -04:00
public RenameJob(Map<File, File> renameMap, net.sourceforge.filebot.RenameAction action) {
this.action = action;
2011-11-04 03:45:48 -04:00
this.renameMap = synchronizedMap(renameMap);
this.renameLog = synchronizedMap(new LinkedHashMap<File, File>());
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
@Override
protected Map<File, File> doInBackground() throws Exception {
try {
for (Entry<File, File> mapping : renameMap.entrySet()) {
if (isCancelled())
return renameLog;
2013-09-30 00:46:33 -04:00
// update progress dialog
firePropertyChange("currentFile", mapping.getKey(), mapping.getValue());
2013-09-30 00:46:33 -04:00
// rename file, throw exception on failure
File source = mapping.getKey();
File destination = resolveDestination(mapping.getKey(), mapping.getValue(), false);
boolean isSameFile = source.equals(destination);
if (!isSameFile || (isSameFile && !source.getName().equals(destination.getName()))) {
action.rename(source, destination);
}
2013-09-30 00:46:33 -04:00
// remember successfully renamed matches for history entry and possible revert
renameLog.put(mapping.getKey(), mapping.getValue());
}
} finally {
postprocess.release();
2011-11-04 03:45:48 -04:00
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
return renameLog;
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
@Override
protected void done() {
try {
postprocess.acquire();
2012-07-26 04:40:20 -04:00
this.get(); // grab exception if any
} catch (Exception e) {
if (!isCancelled()) {
UILogger.log(Level.SEVERE, String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)), e);
} else {
Logger.getLogger(RenameAction.class.getName()).log(Level.SEVERE, e.getMessage(), e);
}
2011-11-04 03:45:48 -04:00
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// collect renamed types
2012-07-26 04:40:20 -04:00
final List<Class<?>> types = new ArrayList<Class<?>>();
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// remove renamed matches
for (File source : renameLog.keySet()) {
// find index of source file
int index = model.files().indexOf(source);
types.add(model.values().get(index).getClass());
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// remove complete match
model.matches().remove(index);
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
if (renameLog.size() > 0) {
UILogger.info(String.format("%d files renamed.", renameLog.size()));
HistorySpooler.getInstance().append(renameLog.entrySet());
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
// count global statistics
2012-07-26 04:40:20 -04:00
for (Class<?> it : new HashSet<Class<?>>(types)) {
2011-11-04 03:45:48 -04:00
Analytics.trackEvent("GUI", "Rename", it.getSimpleName(), frequency(types, it));
}
}
}
2013-09-30 00:46:33 -04:00
2011-11-04 03:45:48 -04:00
@Override
public boolean cancel() {
return cancel(true);
}
}
2013-09-30 00:46:33 -04:00
protected class NativeRenameJob extends RenameJob implements Cancellable {
2013-09-30 00:46:33 -04:00
public NativeRenameJob(Map<File, File> renameMap, NativeRenameAction action) {
super(renameMap, action);
}
2013-09-30 00:46:33 -04:00
@Override
protected Map<File, File> doInBackground() throws Exception {
NativeRenameAction shell = (NativeRenameAction) action;
2013-09-30 00:46:33 -04:00
// prepare delta, ignore files already named as desired
Map<File, File> todo = new LinkedHashMap<File, File>();
for (Entry<File, File> mapping : renameMap.entrySet()) {
File source = mapping.getKey();
File destination = resolveDestination(mapping.getKey(), mapping.getValue(), false);
if (!source.equals(destination)) {
todo.put(source, destination);
}
}
2013-09-30 00:46:33 -04:00
// call native shell move/copy
try {
shell.rename(todo);
} catch (CancellationException e) {
// set as cancelled and propagate the exception
super.cancel(false);
throw e;
} finally {
// check status of renamed files
for (Entry<File, File> it : renameMap.entrySet()) {
if (resolveDestination(it.getKey(), it.getValue(), false).exists()) {
renameLog.put(it.getKey(), it.getValue());
}
}
postprocess.release();
}
2013-09-30 00:46:33 -04:00
return renameLog;
}
2013-09-30 00:46:33 -04:00
@Override
public boolean cancel() {
throw new UnsupportedOperationException();
}
2011-11-04 03:45:48 -04:00
}
2013-09-30 00:46:33 -04:00
}