package net.filebot.ui.rename; import static java.util.Collections.*; import static net.filebot.util.ExceptionUtilities.*; import static net.filebot.util.FileUtilities.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.AbstractMap; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.swing.SwingWorker; import javax.swing.SwingWorker.StateValue; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.TransformedList; import ca.odell.glazedlists.event.ListEvent; import net.filebot.similarity.Match; import net.filebot.util.ui.SwingUI; public class RenameModel extends MatchModel<Object, File> { private final FormattedFutureEventList names = new FormattedFutureEventList(this.values()); private final Map<Object, MatchFormatter> formatters = new LinkedHashMap<Object, MatchFormatter>(); private final MatchFormatter defaultFormatter = new MatchFormatter() { @Override public boolean canFormat(Match<?, ?> match) { return true; } @Override public String preview(Match<?, ?> match) { return replacePathSeparators(String.valueOf(match.getValue())).trim(); // clean up path separators like / or \ } @Override public String format(Match<?, ?> match, boolean extension, Map<?, ?> context) { return preview(match); } }; private boolean preserveExtension = true; public EventList<FormattedFuture> names() { return names; } public EventList<File> files() { return candidates(); } public boolean preserveExtension() { return preserveExtension; } public void setPreserveExtension(boolean preserveExtension) { this.preserveExtension = preserveExtension; // update formatted names names.refresh(); } public Map<File, File> getRenameMap() { Map<File, File> map = new LinkedHashMap<File, File>(); for (int i = 0; i < names.size(); i++) { if (hasComplement(i)) { // make sure we're dealing with regular File objects form here on out File source = new File(files().get(i).getPath()); FormattedFuture task = names.get(i); StringBuilder destination = new StringBuilder(); // append formatted name, throw exception if not ready try { destination.append(task.get(0, TimeUnit.SECONDS)); } catch (ExecutionException e) { throw new IllegalStateException(String.format("\"%s\" could not be formatted: %s.", task.preview(), e.getCause().getMessage())); } catch (TimeoutException e) { throw new IllegalStateException(String.format("\"%s\" has not been formatted yet.", task.preview())); } catch (InterruptedException e) { throw new RuntimeException(e); } // append extension, if desired if (preserveExtension) { String extension = getExtension(source); if (extension != null) { destination.append('.').append(extension.toLowerCase()); } } // insert mapping if (map.put(source, new File(destination.toString())) != null) { throw new IllegalStateException("Duplicate source file: " + source.getName()); } } } return map; } public void useFormatter(Object key, MatchFormatter formatter) { if (formatter != null) { formatters.put(key, formatter); } else { formatters.remove(key); } // reformat matches names.refresh(); } private MatchFormatter getFormatter(Match<Object, File> match) { for (MatchFormatter formatter : formatters.values()) { if (formatter.canFormat(match)) { return formatter; } } return defaultFormatter; } public Map<File, Object> getMatchContext(Match<Object, File> match) { // incomplete matches have no context if (match.getValue() == null || match.getCandidate() == null) { return emptyMap(); } // provide matches context on demand return new AbstractMap<File, Object>() { @Override public Set<Entry<File, Object>> entrySet() { Set<Entry<File, Object>> context = new LinkedHashSet<Entry<File, Object>>(); for (Match<Object, File> it : matches()) { if (it.getValue() != null && it.getCandidate() != null) { context.add(new SimpleImmutableEntry<File, Object>(it.getCandidate(), it.getValue())); } } return context; } }; } private class FormattedFutureEventList extends TransformedList<Object, FormattedFuture> { private final List<FormattedFuture> futures = new ArrayList<FormattedFuture>(); private final Executor backgroundFormatter = new ThreadPoolExecutor(0, 1, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); public FormattedFutureEventList(EventList<Object> source) { super(source); this.source.addListEventListener(this); } @Override public FormattedFuture get(int index) { return futures.get(index); } @Override protected boolean isWritable() { // can't write to source directly return false; } @Override public void add(int index, FormattedFuture value) { source.add(index, value.getMatch().getValue()); } @Override public FormattedFuture set(int index, FormattedFuture value) { FormattedFuture obsolete = get(index); source.set(index, value.getMatch().getValue()); return obsolete; } @Override public FormattedFuture remove(int index) { FormattedFuture obsolete = get(index); source.remove(index); return obsolete; } @Override public void listChanged(ListEvent<Object> listChanges) { updates.beginEvent(true); while (listChanges.next()) { int index = listChanges.getIndex(); int type = listChanges.getType(); if (type == ListEvent.INSERT || type == ListEvent.UPDATE) { Match<Object, File> match = getMatch(index); // create new future FormattedFuture future = new FormattedFuture(match, !preserveExtension, getFormatter(match), getMatchContext(match)); // update data if (type == ListEvent.INSERT) { futures.add(index, future); updates.elementInserted(index, future); } else if (type == ListEvent.UPDATE) { // set new future, dispose old future FormattedFuture obsolete = futures.set(index, future); cancel(obsolete); // Don't update view immediately, to avoid irritating flickering, // caused by a rapid succession of change events. // The worker may only need a couple of milliseconds to complete, // so the view will be notified of the change soon enough. SwingUI.invokeLater(50, new Runnable() { @Override public void run() { // task has not been started, no change events have been sent as of yet, // fire change event now if (future.getState() == StateValue.PENDING) { future.firePropertyChange("state", null, StateValue.PENDING); } } }); } // observe and enqueue worker task submit(future); } else if (type == ListEvent.DELETE) { // remove future from data and formatter queue FormattedFuture obsolete = futures.remove(index); cancel(obsolete); updates.elementDeleted(index, obsolete); } } updates.commitEvent(); } public void refresh() { updates.beginEvent(true); for (int i = 0; i < size(); i++) { FormattedFuture obsolete = futures.get(i); Match<Object, File> match = obsolete.getMatch(); FormattedFuture future = new FormattedFuture(match, !preserveExtension, getFormatter(match), getMatchContext(match)); // replace and cancel old future cancel(futures.set(i, future)); // submit new future submit(future); updates.elementUpdated(i, obsolete, future); } updates.commitEvent(); } private void submit(FormattedFuture future) { // observe and enqueue worker task future.addPropertyChangeListener(futureListener); backgroundFormatter.execute(future); } private void cancel(FormattedFuture future) { // remove listener and cancel worker task future.removePropertyChangeListener(futureListener); future.cancel(true); } private final PropertyChangeListener futureListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { int index = futures.indexOf(evt.getSource()); // sanity check if (index >= 0 && index < size()) { FormattedFuture future = (FormattedFuture) evt.getSource(); updates.beginEvent(true); updates.elementUpdated(index, future, future); updates.commitEvent(); } } }; } public static class FormattedFuture extends SwingWorker<String, Void> { private final Match<Object, File> match; private final boolean extension; private final Map<File, Object> context; private final MatchFormatter formatter; private FormattedFuture(Match<Object, File> match, boolean extension, MatchFormatter formatter, Map<File, Object> context) { this.match = match; this.extension = extension; this.formatter = formatter; this.context = context; } public boolean isComplexFormat() { return formatter instanceof ExpressionFormatter; } public Match<Object, File> getMatch() { return match; } public String preview() { return formatter.preview(match).trim(); } @Override protected String doInBackground() throws Exception { return formatter.format(match, extension, context).trim(); } @Override public String toString() { if (isDone()) { try { return get(0, TimeUnit.SECONDS); } catch (Throwable t) { // find the original exception if (t.getCause() != null && t instanceof ExecutionException) { t = t.getCause(); } return String.format("[%s] %s", getMessage(t), preview()); } } // use preview if we are not ready yet return preview(); } } }