+ Script expressions in ExpressionFormat will now be evaluated in a secure sandbox

+ "preserve Extension" can be enabled/disabled in RenameModel

* fixed rename list SelectionModel performance issue 
* create package for ui-independant Hash* stuff
This commit is contained in:
Reinhard Pointner 2009-05-02 23:34:04 +00:00
parent 9e60d2c5dd
commit ca032f3b56
39 changed files with 674 additions and 346 deletions

View File

@ -106,7 +106,7 @@
<!-- <!--
Mandatory Default Cache configuration. These settings will be applied to caches Mandatory Default Cache configuration. These settings will be applied to caches
created programmtically using CacheManager.add(String cacheName). created pragmatically using CacheManager.add(String cacheName).
--> -->
<defaultCache <defaultCache
maxElementsInMemory="100" maxElementsInMemory="100"

View File

@ -23,9 +23,6 @@ public class ArgumentBean {
@Option(name = "-clear", usage = "Clear history and settings") @Option(name = "-clear", usage = "Clear history and settings")
private boolean clear = false; private boolean clear = false;
@Option(name = "--analyze", usage = "Open file in 'Analyze' panel", metaVar = "<file>")
private boolean analyze;
@Option(name = "--sfv", usage = "Open file in 'SFV' panel", metaVar = "<file>") @Option(name = "--sfv", usage = "Open file in 'SFV' panel", metaVar = "<file>")
private boolean sfv; private boolean sfv;
@ -48,11 +45,6 @@ public class ArgumentBean {
} }
public boolean analyze() {
return analyze;
}
public List<File> arguments() { public List<File> arguments() {
return arguments; return arguments;
} }

View File

@ -4,6 +4,12 @@ package net.sourceforge.filebot;
import static javax.swing.JFrame.EXIT_ON_CLOSE; import static javax.swing.JFrame.EXIT_ON_CLOSE;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.util.logging.ConsoleHandler; import java.util.logging.ConsoleHandler;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -12,10 +18,10 @@ import javax.swing.JFrame;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.UIManager; import javax.swing.UIManager;
import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.ui.MainFrame; import net.sourceforge.filebot.ui.MainFrame;
import net.sourceforge.filebot.ui.NotificationLoggingHandler; import net.sourceforge.filebot.ui.NotificationLoggingHandler;
import net.sourceforge.filebot.ui.SinglePanelFrame; import net.sourceforge.filebot.ui.SinglePanelFrame;
import net.sourceforge.filebot.ui.panel.analyze.AnalyzePanelBuilder;
import net.sourceforge.filebot.ui.panel.sfv.SfvPanelBuilder; import net.sourceforge.filebot.ui.panel.sfv.SfvPanelBuilder;
import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineException;
@ -45,6 +51,7 @@ public class Main {
initializeLogging(); initializeLogging();
initializeSettings(); initializeSettings();
initializeSecurityManager();
try { try {
// use native laf an all platforms // use native laf an all platforms
@ -59,19 +66,18 @@ public class Main {
public void run() { public void run() {
JFrame frame; JFrame frame;
if (argumentBean.analyze()) { if (argumentBean.sfv()) {
frame = new SinglePanelFrame(new AnalyzePanelBuilder()).publish(argumentBean.transferable()); // sfv frame
} else if (argumentBean.sfv()) {
frame = new SinglePanelFrame(new SfvPanelBuilder()).publish(argumentBean.transferable()); frame = new SinglePanelFrame(new SfvPanelBuilder()).publish(argumentBean.transferable());
} else { } else {
// default // default frame
frame = new MainFrame(); frame = new MainFrame();
} }
frame.setLocationByPlatform(true); frame.setLocationByPlatform(true);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE); frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
// start // start application
frame.setVisible(true); frame.setVisible(true);
} }
}); });
@ -95,11 +101,51 @@ public class Main {
} }
/**
* Preset the default thetvdb.apikey.
*/
private static void initializeSettings() { private static void initializeSettings() {
Settings.userRoot().putDefault("thetvdb.apikey", "58B4AA94C59AD656"); Settings.userRoot().putDefault("thetvdb.apikey", "58B4AA94C59AD656");
} }
/**
* Initialize default SecurityManager and grant all permissions via security policy.
* Initialization is required in order to run {@link ExpressionFormat} in a secure sandbox.
*/
private static void initializeSecurityManager() {
try {
// initialize security policy used by the default security manager
// because default the security policy is very restrictive (e.g. no FilePermission)
Policy.setPolicy(new Policy() {
@Override
public boolean implies(ProtectionDomain domain, Permission permission) {
// all permissions
return true;
}
@Override
public PermissionCollection getPermissions(CodeSource codesource) {
// VisualVM can't connect if this method does return
// a checked immutable PermissionCollection
return new Permissions();
}
});
// set default security manager
System.setSecurityManager(new SecurityManager());
} catch (Exception e) {
// security manager was probably set via system property
Logger.getLogger(Main.class.getName()).log(Level.WARNING, e.toString(), e);
}
}
/**
* Parse command line arguments.
*/
private static ArgumentBean initializeArgumentBean(String... args) throws CmdLineException { private static ArgumentBean initializeArgumentBean(String... args) throws CmdLineException {
ArgumentBean argumentBean = new ArgumentBean(); ArgumentBean argumentBean = new ArgumentBean();
@ -109,6 +155,9 @@ public class Main {
} }
/**
* Print command line argument usage.
*/
private static void printUsage(ArgumentBean argumentBean) { private static void printUsage(ArgumentBean argumentBean) {
System.out.println("Options:"); System.out.println("Options:");

View File

@ -2,6 +2,7 @@
package net.sourceforge.filebot.format; package net.sourceforge.filebot.format;
import static net.sourceforge.filebot.FileBotUtilities.SFV_FILES;
import static net.sourceforge.filebot.format.Define.undefined; import static net.sourceforge.filebot.format.Define.undefined;
import java.io.File; import java.io.File;
@ -9,15 +10,21 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Scanner; import java.util.Scanner;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import net.sf.ehcache.Cache; import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager; import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element; import net.sf.ehcache.Element;
import net.sourceforge.filebot.FileBotUtilities; import net.sourceforge.filebot.FileBotUtilities;
import net.sourceforge.filebot.hash.IllegalSyntaxException;
import net.sourceforge.filebot.hash.SfvFileScanner;
import net.sourceforge.filebot.mediainfo.MediaInfo; import net.sourceforge.filebot.mediainfo.MediaInfo;
import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind; import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind;
import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Episode;
import net.sourceforge.tuned.FileUtilities;
public class EpisodeFormatBindingBean { public class EpisodeFormatBindingBean {
@ -78,6 +85,16 @@ public class EpisodeFormatBindingBean {
} }
@Define("cf")
public String getContainerFormat() {
// container format extension
String extensions = getMediaInfo(StreamKind.General, 0, "Codec/Extensions");
// get first token
return new Scanner(extensions).next();
}
@Define("hi") @Define("hi")
public String getHeightAndInterlacement() { public String getHeightAndInterlacement() {
String height = getMediaInfo(StreamKind.Video, 0, "Height"); String height = getMediaInfo(StreamKind.Video, 0, "Height");
@ -91,15 +108,6 @@ public class EpisodeFormatBindingBean {
} }
@Define("ext")
public String getContainerExtension() {
String extensions = getMediaInfo(StreamKind.General, 0, "Codec/Extensions");
// get first token
return new Scanner(extensions).next();
}
@Define("resolution") @Define("resolution")
public String getVideoResolution() { public String getVideoResolution() {
String width = getMediaInfo(StreamKind.Video, 0, "Width"); String width = getMediaInfo(StreamKind.Video, 0, "Width");
@ -117,11 +125,16 @@ public class EpisodeFormatBindingBean {
public String getCRC32() throws IOException, InterruptedException { public String getCRC32() throws IOException, InterruptedException {
if (mediaFile != null) { if (mediaFile != null) {
// try to get checksum from file name // try to get checksum from file name
String embeddedChecksum = FileBotUtilities.getEmbeddedChecksum(mediaFile.getName()); String checksum = FileBotUtilities.getEmbeddedChecksum(mediaFile.getName());
if (embeddedChecksum != null) { if (checksum != null)
return embeddedChecksum; return checksum;
}
// try to get checksum from sfv file
checksum = getChecksumFromSfvFile(mediaFile);
if (checksum != null)
return checksum;
// calculate checksum from file // calculate checksum from file
return crc32(mediaFile); return crc32(mediaFile);
@ -131,6 +144,13 @@ public class EpisodeFormatBindingBean {
} }
@Define("ext")
public String getContainerExtension() {
// file extension
return FileUtilities.getExtension(mediaFile);
}
@Define("general") @Define("general")
public Object getGeneralMediaInfo() { public Object getGeneralMediaInfo() {
return new AssociativeScriptObject(getMediaInfo().snapshot(StreamKind.General, 0)); return new AssociativeScriptObject(getMediaInfo().snapshot(StreamKind.General, 0));
@ -161,11 +181,13 @@ public class EpisodeFormatBindingBean {
} }
@Define("episode")
public Episode getEpisode() { public Episode getEpisode() {
return episode; return episode;
} }
@Define("file")
public File getMediaFile() { public File getMediaFile() {
return mediaFile; return mediaFile;
} }
@ -201,6 +223,33 @@ public class EpisodeFormatBindingBean {
} }
private String getChecksumFromSfvFile(File mediaFile) throws IOException {
File folder = mediaFile.getParentFile();
for (File sfvFile : folder.listFiles(SFV_FILES)) {
SfvFileScanner scanner = new SfvFileScanner(sfvFile);
try {
while (scanner.hasNext()) {
try {
Entry<File, String> entry = scanner.next();
if (mediaFile.getName().equals(entry.getKey().getPath())) {
return entry.getValue();
}
} catch (IllegalSyntaxException e) {
Logger.getLogger("global").log(Level.WARNING, e.getMessage());
}
}
} finally {
scanner.close();
}
}
return null;
}
private String crc32(File file) throws IOException, InterruptedException { private String crc32(File file) throws IOException, InterruptedException {
// try to get checksum from cache // try to get checksum from cache
Cache cache = CacheManager.getInstance().getCache("checksum"); Cache cache = CacheManager.getInstance().getCache("checksum");

View File

@ -16,16 +16,16 @@ import net.sourceforge.tuned.ExceptionUtilities;
public class ExpressionBindings extends AbstractMap<String, Object> implements Bindings { public class ExpressionBindings extends AbstractMap<String, Object> implements Bindings {
protected final Object bean; protected final Object bindingBean;
protected final Map<String, Method> bindings = new HashMap<String, Method>(); protected final Map<String, Method> bindings = new HashMap<String, Method>();
public ExpressionBindings(Object bindingBean) { public ExpressionBindings(Object bindingBean) {
bean = bindingBean; this.bindingBean = bindingBean;
// get method bindings // get method bindings
for (Method method : bean.getClass().getMethods()) { for (Method method : bindingBean.getClass().getMethods()) {
Define define = method.getAnnotation(Define.class); Define define = method.getAnnotation(Define.class);
if (define != null) { if (define != null) {
@ -41,19 +41,19 @@ public class ExpressionBindings extends AbstractMap<String, Object> implements B
public Object getBindingBean() { public Object getBindingBean() {
return bean; return bindingBean;
} }
protected Object evaluate(Method method) throws Exception { protected Object evaluate(final Method method) throws Exception {
Object value = method.invoke(getBindingBean()); Object value = method.invoke(bindingBean);
if (value != null) { if (value != null) {
return value; return value;
} }
// invoke fallback method // invoke fallback method
return bindings.get(Define.undefined).invoke(getBindingBean()); return bindings.get(Define.undefined).invoke(bindingBean);
} }

View File

@ -0,0 +1,31 @@
package net.sourceforge.filebot.format;
import javax.script.ScriptException;
public class ExpressionException extends ScriptException {
private final String message;
public ExpressionException(String message, Exception cause) {
super(cause);
// can't set message via super constructor
this.message = message;
}
public ExpressionException(Exception e) {
this(e.getMessage(), e);
}
@Override
public String getMessage() {
return message;
}
}

View File

@ -2,12 +2,26 @@
package net.sourceforge.filebot.format; package net.sourceforge.filebot.format;
import java.io.FilePermission;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.security.AccessControlContext;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.text.FieldPosition; import java.text.FieldPosition;
import java.text.Format; import java.text.Format;
import java.text.ParsePosition; import java.text.ParsePosition;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.PropertyPermission;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -19,6 +33,10 @@ import javax.script.ScriptEngine;
import javax.script.ScriptException; import javax.script.ScriptException;
import javax.script.SimpleScriptContext; import javax.script.SimpleScriptContext;
import net.sourceforge.tuned.ExceptionUtilities;
import org.mozilla.javascript.EcmaError;
import com.sun.phobos.script.javascript.RhinoScriptEngine; import com.sun.phobos.script.javascript.RhinoScriptEngine;
@ -33,7 +51,7 @@ public class ExpressionFormat extends Format {
public ExpressionFormat(String expression) throws ScriptException { public ExpressionFormat(String expression) throws ScriptException {
this.expression = expression; this.expression = expression;
this.compilation = compile(expression, (Compilable) initScriptEngine()); this.compilation = secure(compile(expression, (Compilable) initScriptEngine()));
} }
@ -97,7 +115,9 @@ public class ExpressionFormat extends Format {
public StringBuffer format(Bindings bindings, StringBuffer sb) { public StringBuffer format(Bindings bindings, StringBuffer sb) {
ScriptContext context = new SimpleScriptContext(); ScriptContext context = new SimpleScriptContext();
context.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
// use privileged bindings so we are not restricted by the script sandbox
context.setBindings(PrivilegedBindings.newProxy(bindings), ScriptContext.GLOBAL_SCOPE);
for (Object snipped : compilation) { for (Object snipped : compilation) {
if (snipped instanceof CompiledScript) { if (snipped instanceof CompiledScript) {
@ -108,9 +128,16 @@ public class ExpressionFormat extends Format {
sb.append(value); sb.append(value);
} }
} catch (ScriptException e) { } catch (ScriptException e) {
EcmaError ecmaError = ExceptionUtilities.findCause(e, EcmaError.class);
// try to unwrap EcmaError
if (ecmaError != null) {
lastException = new ExpressionException(String.format("%s: %s", ecmaError.getName(), ecmaError.getErrorMessage()), e);
} else {
lastException = e; lastException = e;
} catch (Exception e) { }
lastException = new ScriptException(e); } catch (RuntimeException e) {
lastException = new ExpressionException(e);
} }
} else { } else {
sb.append(snipped); sb.append(snipped);
@ -126,6 +153,123 @@ public class ExpressionFormat extends Format {
} }
private Object[] secure(Object[] compilation) {
// create sandbox AccessControlContext
AccessControlContext sandbox = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, getSandboxPermissions()) });
for (int i = 0; i < compilation.length; i++) {
Object snipped = compilation[i];
if (snipped instanceof CompiledScript) {
compilation[i] = new SecureCompiledScript(sandbox, (CompiledScript) snipped);
}
}
return compilation;
}
private PermissionCollection getSandboxPermissions() {
Permissions permissions = new Permissions();
permissions.add(new RuntimePermission("createClassLoader"));
permissions.add(new FilePermission("<<ALL FILES>>", "read"));
permissions.add(new PropertyPermission("*", "read"));
permissions.add(new RuntimePermission("getenv.*"));
return permissions;
}
private static class PrivilegedBindings implements InvocationHandler {
private final Bindings bindings;
private PrivilegedBindings(Bindings bindings) {
this.bindings = bindings;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
return method.invoke(bindings, args);
}
});
} catch (PrivilegedActionException e) {
Throwable cause = e.getException();
// the underlying method may have throw an exception
if (cause instanceof InvocationTargetException) {
// get actual cause
cause = cause.getCause();
}
// forward cause
throw cause;
}
}
public static Bindings newProxy(Bindings bindings) {
PrivilegedBindings invocationHandler = new PrivilegedBindings(bindings);
// create dynamic invocation proxy
return (Bindings) Proxy.newProxyInstance(PrivilegedBindings.class.getClassLoader(), new Class[] { Bindings.class }, invocationHandler);
}
}
private static class SecureCompiledScript extends CompiledScript {
private final AccessControlContext sandbox;
private final CompiledScript compiledScript;
private SecureCompiledScript(AccessControlContext sandbox, CompiledScript compiledScript) {
this.sandbox = sandbox;
this.compiledScript = compiledScript;
}
@Override
public Object eval(final ScriptContext context) throws ScriptException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws ScriptException {
return compiledScript.eval(context);
}
}, sandbox);
} catch (PrivilegedActionException e) {
AccessControlException accessException = ExceptionUtilities.findCause(e, AccessControlException.class);
// try to unwrap AccessControlException
if (accessException != null)
throw new ExpressionException(accessException);
// forward ScriptException
// e.getException() should be an instance of ScriptException,
// as only "checked" exceptions will be "wrapped" in a PrivilegedActionException
throw (ScriptException) e.getException();
}
}
@Override
public ScriptEngine getEngine() {
return compiledScript.getEngine();
}
}
@Override @Override
public Object parseObject(String source, ParsePosition pos) { public Object parseObject(String source, ParsePosition pos) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -1,11 +1,11 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
import java.util.zip.Checksum; import java.util.zip.Checksum;
class ChecksumHash implements Hash { public class ChecksumHash implements Hash {
private final Checksum checksum; private final Checksum checksum;

View File

@ -1,8 +1,8 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
interface Hash { public interface Hash {
public void update(byte[] bytes, int off, int len); public void update(byte[] bytes, int off, int len);

View File

@ -1,17 +1,13 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
import java.io.File;
import java.util.Formatter; import java.util.Formatter;
import java.util.Scanner; import java.util.Scanner;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32; import java.util.zip.CRC32;
enum HashType { public enum HashType {
SFV { SFV {
@ -23,44 +19,13 @@ enum HashType {
@Override @Override
public VerificationFileScanner newScanner(Scanner scanner) { public VerificationFileScanner newScanner(Scanner scanner) {
// adapt default scanner to sfv line syntax return new SfvFileScanner(scanner);
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 @Override
public VerificationFilePrinter newPrinter(Formatter out) { public VerificationFilePrinter newPrinter(Formatter out) {
return new VerificationFilePrinter(out, "CRC32") { return new SfvFilePrinter(out);
@Override
public void print(String path, String hash) {
// e.g folder/file.txt 970E4EF1
out.format(String.format("%s %s", path, hash));
}
};
} }
}, },

View File

@ -0,0 +1,16 @@
package net.sourceforge.filebot.hash;
public 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

@ -1,5 +1,5 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
import java.math.BigInteger; import java.math.BigInteger;
@ -7,7 +7,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
class MessageDigestHash implements Hash { public class MessageDigestHash implements Hash {
private final MessageDigest md; private final MessageDigest md;

View File

@ -0,0 +1,20 @@
package net.sourceforge.filebot.hash;
import java.util.Formatter;
public class SfvFilePrinter extends VerificationFilePrinter {
public SfvFilePrinter(Formatter out) {
super(out, "CRC32");
}
@Override
public void println(String path, String hash) {
// e.g folder/file.txt 970E4EF1
out.format(String.format("%s %s%n", path, hash));
}
}

View File

@ -0,0 +1,46 @@
package net.sourceforge.filebot.hash;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SfvFileScanner extends VerificationFileScanner {
public SfvFileScanner(File file) throws FileNotFoundException {
super(file);
}
public SfvFileScanner(Scanner scanner) {
super(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) throws IllegalSyntaxException {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches())
throw new IllegalSyntaxException(getLineNumber(), line);
return entry(new File(matcher.group(1)), matcher.group(2));
}
}

View File

@ -1,5 +1,5 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
import java.io.Closeable; import java.io.Closeable;
@ -7,7 +7,7 @@ import java.io.IOException;
import java.util.Formatter; import java.util.Formatter;
class VerificationFilePrinter implements Closeable { public class VerificationFilePrinter implements Closeable {
protected final Formatter out; protected final Formatter out;
protected final String algorithm; protected final String algorithm;
@ -20,17 +20,8 @@ class VerificationFilePrinter implements Closeable {
public void println(String path, String hash) { 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 // e.g. 1a02a7c1e9ac91346d08829d5037b240f42ded07 ?SHA1*folder/file.txt
out.format("%s %s*%s", hash, algorithm == null ? "" : '?' + algorithm.toUpperCase(), path); out.format("%s %s*%s%n", hash, algorithm == null ? "" : '?' + algorithm.toUpperCase(), path);
} }

View File

@ -1,5 +1,5 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
import java.io.Closeable; import java.io.Closeable;
@ -16,7 +16,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeable { public class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeable {
private final Scanner scanner; private final Scanner scanner;
@ -48,7 +48,7 @@ class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeabl
@Override @Override
public Entry<File, String> next() { public Entry<File, String> next() throws IllegalSyntaxException {
// cache next line // cache next line
if (!hasNext()) { if (!hasNext()) {
throw new NoSuchElementException(); throw new NoSuchElementException();
@ -88,10 +88,10 @@ class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeabl
* | Group 1 | | Group 2 | * | Group 1 | | Group 2 |
* </pre> * </pre>
*/ */
private final Pattern pattern = Pattern.compile("(\\p{XDigit}{8,})\\s+(?:\\?\\w+)?\\*(.+)"); private final Pattern pattern = Pattern.compile("(\\p{XDigit}+)\\s+(?:\\?\\w+)?\\*(.+)");
protected Entry<File, String> parseLine(String line) { protected Entry<File, String> parseLine(String line) throws IllegalSyntaxException {
Matcher matcher = pattern.matcher(line); Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) if (!matcher.matches())
@ -127,18 +127,4 @@ class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeabl
throw new UnsupportedOperationException(); 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

@ -2,11 +2,11 @@
package net.sourceforge.filebot.torrent; package net.sourceforge.filebot.torrent;
import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.FileChannel; import java.io.InputStream;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -16,8 +16,6 @@ import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import net.sourceforge.tuned.ByteBufferInputStream;
public class Torrent { public class Torrent {
@ -107,13 +105,12 @@ public class Torrent {
private static Map<?, ?> decodeTorrent(File torrent) throws IOException { private static Map<?, ?> decodeTorrent(File torrent) throws IOException {
FileChannel fileChannel = new FileInputStream(torrent).getChannel(); InputStream in = new BufferedInputStream(new FileInputStream(torrent));
try { try {
// memory-map and decode torrent return BDecoder.decode(in);
return BDecoder.decode(new ByteBufferInputStream(fileChannel.map(MapMode.READ_ONLY, 0, fileChannel.size())));
} finally { } finally {
fileChannel.close(); in.close();
} }
} }
@ -206,6 +203,12 @@ public class Torrent {
public String getPath() { public String getPath() {
return path; return path;
} }
@Override
public String toString() {
return getPath();
}
} }
} }

View File

@ -17,7 +17,9 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.io.File; import java.io.File;
import java.text.ParseException; import java.text.ParseException;
import java.util.Arrays; import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -53,6 +55,7 @@ import net.sourceforge.filebot.format.EpisodeFormatBindingBean;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.Episode.EpisodeFormat; import net.sourceforge.filebot.web.Episode.EpisodeFormat;
import net.sourceforge.tuned.DefaultThreadFactory;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.ui.GradientStyle; import net.sourceforge.tuned.ui.GradientStyle;
import net.sourceforge.tuned.ui.LinkButton; import net.sourceforge.tuned.ui.LinkButton;
@ -68,8 +71,7 @@ public class EpisodeFormatDialog extends JDialog {
private JLabel preview = new JLabel(); private JLabel preview = new JLabel();
private JLabel warningMessage = new JLabel(ResourceManager.getIcon("status.warning")); private JLabel status = new JLabel();
private JLabel errorMessage = new JLabel(ResourceManager.getIcon("status.error"));
private EpisodeFormatBindingBean previewSample = new EpisodeFormatBindingBean(getPreviewSampleEpisode(), getPreviewSampleMediaFile()); private EpisodeFormatBindingBean previewSample = new EpisodeFormatBindingBean(getPreviewSampleEpisode(), getPreviewSampleMediaFile());
@ -107,15 +109,10 @@ public class EpisodeFormatDialog extends JDialog {
header.setBackground(Color.white); header.setBackground(Color.white);
header.setBorder(new SeparatorBorder(1, new Color(0xB4B4B4), new Color(0xACACAC), GradientStyle.LEFT_TO_RIGHT, Position.BOTTOM)); header.setBorder(new SeparatorBorder(1, new Color(0xB4B4B4), new Color(0xACACAC), GradientStyle.LEFT_TO_RIGHT, Position.BOTTOM));
errorMessage.setVisible(false);
warningMessage.setVisible(false);
progressIndicator.setVisible(false);
header.add(progressIndicator, "pos 1al 0al, hidemode 3"); header.add(progressIndicator, "pos 1al 0al, hidemode 3");
header.add(title, "wrap unrel:push"); header.add(title, "wrap unrel:push");
header.add(preview, "gap indent, hidemode 3, wmax 90%"); header.add(preview, "hmin 16px, gap indent, hidemode 3, wmax 90%");
header.add(errorMessage, "gap indent, hidemode 3, wmax 90%, newline"); header.add(status, "hmin 16px, gap indent, hidemode 3, wmax 90%, newline");
header.add(warningMessage, "gap indent, hidemode 3, wmax 90%, newline");
JPanel content = new JPanel(new MigLayout("insets dialog, nogrid, fill")); JPanel content = new JPanel(new MigLayout("insets dialog, nogrid, fill"));
@ -125,7 +122,7 @@ public class EpisodeFormatDialog extends JDialog {
content.add(createSyntaxPanel(), "gapx indent indent, wrap 8px"); content.add(createSyntaxPanel(), "gapx indent indent, wrap 8px");
content.add(new JLabel("Examples"), "gap indent+unrel, wrap 0"); content.add(new JLabel("Examples"), "gap indent+unrel, wrap 0");
content.add(createExamplesPanel(), "gapx indent indent, wrap 25px:push"); content.add(createExamplesPanel(), "hmin 50px, gapx indent indent, wrap 25px:push");
content.add(new JButton(useDefaultFormatAction), "tag left"); content.add(new JButton(useDefaultFormatAction), "tag left");
content.add(new JButton(approveFormatAction), "tag apply"); content.add(new JButton(approveFormatAction), "tag apply");
@ -137,12 +134,8 @@ public class EpisodeFormatDialog extends JDialog {
pane.add(header, "h 60px, growx, dock north"); pane.add(header, "h 60px, growx, dock north");
pane.add(content, "grow"); pane.add(content, "grow");
setSize(485, 415);
header.setComponentPopupMenu(createPreviewSamplePopup()); header.setComponentPopupMenu(createPreviewSamplePopup());
setLocation(TunedUtilities.getPreferredLocation(this));
// update format on change // update format on change
editor.getDocument().addDocumentListener(new LazyDocumentAdapter() { editor.getDocument().addDocumentListener(new LazyDocumentAdapter() {
@ -171,6 +164,10 @@ public class EpisodeFormatDialog extends JDialog {
// update preview to current format // update preview to current format
firePreviewSampleChanged(); firePreviewSampleChanged();
// initialize window properties
setLocation(TunedUtilities.getPreferredLocation(this));
pack();
} }
@ -244,30 +241,58 @@ public class EpisodeFormatDialog extends JDialog {
} }
private JPanel createExamplesPanel() { private JComponent createExamplesPanel() {
JPanel panel = new JPanel(new MigLayout("fill, wrap 3")); JPanel panel = new JPanel(new MigLayout("fill, wrap 3"));
panel.setBorder(new LineBorder(new Color(0xACA899))); panel.setBorder(new LineBorder(new Color(0xACA899)));
panel.setBackground(new Color(0xFFFFE1)); panel.setBackground(new Color(0xFFFFE1));
panel.setOpaque(true);
ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName()); ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName());
// sort keys // collect example keys
String[] keys = bundle.keySet().toArray(new String[0]); List<String> examples = new ArrayList<String>();
Arrays.sort(keys);
for (String key : keys) { for (String key : bundle.keySet()) {
if (key.startsWith("example")) { if (key.startsWith("example"))
String format = bundle.getString(key); examples.add(key);
}
// sort by example key
Collections.sort(examples);
for (String key : examples) {
final String format = bundle.getString(key);
LinkButton formatLink = new LinkButton(new AbstractAction(format) {
@Override
public void actionPerformed(ActionEvent e) {
editor.setText(format);
}
});
LinkButton formatLink = new LinkButton(new ExampleFormatAction(format));
formatLink.setFont(new Font(MONOSPACED, PLAIN, 11)); formatLink.setFont(new Font(MONOSPACED, PLAIN, 11));
final JLabel formatExample = new JLabel();
// bind text to preview
addPropertyChangeListener("previewSample", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
try {
formatExample.setText(new ExpressionFormat(format).format(previewSample));
setForeground(defaultColor);
} catch (Exception e) {
formatExample.setText(ExceptionUtilities.getRootCauseMessage(e));
setForeground(errorColor);
}
}
});
panel.add(formatLink); panel.add(formatLink);
panel.add(new JLabel("...")); panel.add(new JLabel("..."));
panel.add(new ExampleFormatLabel(format)); panel.add(formatExample);
}
} }
return panel; return panel;
@ -307,7 +332,31 @@ public class EpisodeFormatDialog extends JDialog {
private ExecutorService createPreviewExecutor() { private ExecutorService createPreviewExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1)); ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), new DefaultThreadFactory("PreviewFormatter")) {
@SuppressWarnings("deprecation")
@Override
public List<Runnable> shutdownNow() {
List<Runnable> remaining = super.shutdownNow();
try {
if (!awaitTermination(3, TimeUnit.SECONDS)) {
// if the thread has not terminated after 4 seconds, it is probably stuck
ThreadGroup threadGroup = ((DefaultThreadFactory) getThreadFactory()).getThreadGroup();
// kill background thread by force
threadGroup.stop();
// log access of potentially unsafe method
Logger.getLogger("global").warning("Thread was forcibly terminated");
}
} catch (InterruptedException e) {
Logger.getLogger("global").log(Level.WARNING, "Thread was not terminated", e);
}
return remaining;
}
};
// only keep the latest task in the queue // only keep the latest task in the queue
executor.setRejectedExecutionHandler(new DiscardOldestPolicy()); executor.setRejectedExecutionHandler(new DiscardOldestPolicy());
@ -345,34 +394,33 @@ public class EpisodeFormatDialog extends JDialog {
// check internal script exception and empty output // check internal script exception and empty output
if (format.scriptException() != null) { if (format.scriptException() != null) {
warningMessage.setText(format.scriptException().getCause().getMessage()); throw format.scriptException();
} else if (get().trim().isEmpty()) { } else if (get().trim().isEmpty()) {
warningMessage.setText("Formatted value is empty"); throw new RuntimeException("Formatted value is empty");
} else {
warningMessage.setText(null);
} }
// no warning or error
status.setVisible(false);
} catch (Exception e) { } catch (Exception e) {
Logger.getLogger("global").log(Level.WARNING, e.getMessage(), e); status.setText(ExceptionUtilities.getMessage(e));
} status.setIcon(ResourceManager.getIcon("status.warning"));
status.setVisible(true);
} finally {
preview.setVisible(true); preview.setVisible(true);
warningMessage.setVisible(warningMessage.getText() != null);
errorMessage.setVisible(false);
editor.setForeground(defaultColor); editor.setForeground(defaultColor);
progressIndicatorTimer.stop(); progressIndicatorTimer.stop();
progressIndicator.setVisible(false); progressIndicator.setVisible(false);
} }
}
}); });
} catch (ScriptException e) { } catch (ScriptException e) {
// incorrect syntax // incorrect syntax
errorMessage.setText(ExceptionUtilities.getRootCauseMessage(e)); status.setText(ExceptionUtilities.getRootCauseMessage(e));
errorMessage.setVisible(true); status.setIcon(ResourceManager.getIcon("status.error"));
status.setVisible(true);
preview.setVisible(false); preview.setVisible(false);
warningMessage.setVisible(false);
editor.setForeground(errorColor); editor.setForeground(errorColor);
} }
} }
@ -418,6 +466,9 @@ public class EpisodeFormatDialog extends JDialog {
@Override @Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
try { try {
if (progressIndicator.isVisible())
throw new IllegalStateException("Format has not been verified yet.");
// check syntax // check syntax
new ExpressionFormat(editor.getText()); new ExpressionFormat(editor.getText());
@ -425,8 +476,8 @@ public class EpisodeFormatDialog extends JDialog {
Settings.userRoot().put("dialog.format", editor.getText()); Settings.userRoot().put("dialog.format", editor.getText());
finish(Option.APPROVE); finish(Option.APPROVE);
} catch (ScriptException e) { } catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e); Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e));
} }
} }
}; };
@ -437,42 +488,6 @@ public class EpisodeFormatDialog extends JDialog {
} }
protected class ExampleFormatAction extends AbstractAction {
public ExampleFormatAction(String format) {
super(format);
}
@Override
public void actionPerformed(ActionEvent e) {
editor.setText(getValue(Action.NAME).toString());
}
}
protected class ExampleFormatLabel extends JLabel {
public ExampleFormatLabel(final String format) {
// bind text to preview
EpisodeFormatDialog.this.addPropertyChangeListener("previewSample", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
try {
setText(new ExpressionFormat(format).format(previewSample));
setForeground(defaultColor);
} catch (Exception e) {
setText(ExceptionUtilities.getRootCauseMessage(e));
setForeground(errorColor);
}
}
});
}
}
protected static abstract class LazyDocumentAdapter implements DocumentListener { protected static abstract class LazyDocumentAdapter implements DocumentListener {
private final Timer timer = new Timer(200, new ActionListener() { private final Timer timer = new Timer(200, new ActionListener() {

View File

@ -3,11 +3,11 @@ syntax: <html><b>{</b> <b>}</b> ... expression, <b>n</b> ... name, <b>s</b> ...
# basic 1.01 # basic 1.01
example[0]: {n} - {s}.{e} - {t} example[0]: {n} - {s}.{e} - {t}
# 1x01
example[1]: {n} - {s+'x'}{e.pad(2)}
# S01E01 # S01E01
example[2]: {n} - {'S'+s.pad(2)}E{e.pad(2)} example[1]: {n} - {'S'+s.pad(2)}E{e.pad(2)} - {t}
# 1x01
example[2]: {n} - {s+'x'}{e.pad(2)}
# uglyfy name # uglyfy name
example[3]: {n.space('.').toLowerCase()} example[3]: {n.space('.').toLowerCase()}.{s}{e.pad(2)}

View File

@ -51,7 +51,7 @@ class MatchAction extends AbstractAction {
this.model = model; this.model = model;
this.metrics = createMetrics(); this.metrics = createMetrics();
putValue(SHORT_DESCRIPTION, "Match names to files"); putValue(SHORT_DESCRIPTION, "Match files and names");
} }

View File

@ -15,7 +15,7 @@ import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEvent;
class MatchModel<Value, Candidate> { public class MatchModel<Value, Candidate> {
private final EventList<Match<Value, Candidate>> source = new BasicEventList<Match<Value, Candidate>>(); private final EventList<Match<Value, Candidate>> source = new BasicEventList<Match<Value, Candidate>>();

View File

@ -28,17 +28,15 @@ import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy;
import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Episode;
import net.sourceforge.tuned.FastFile; import net.sourceforge.tuned.FastFile;
import ca.odell.glazedlists.EventList;
class NamesListTransferablePolicy extends FileTransferablePolicy { class NamesListTransferablePolicy extends FileTransferablePolicy {
private static final DataFlavor episodeArrayFlavor = ArrayTransferable.flavor(Episode.class); private static final DataFlavor episodeArrayFlavor = ArrayTransferable.flavor(Episode.class);
private final EventList<Object> model; private final List<Object> model;
public NamesListTransferablePolicy(EventList<Object> model) { public NamesListTransferablePolicy(List<Object> model) {
this.model = model; this.model = model;
} }

View File

@ -5,16 +5,15 @@ package net.sourceforge.filebot.ui.panel.rename;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque; import java.util.ArrayList;
import java.util.Deque; import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.swing.AbstractAction; import javax.swing.AbstractAction;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.FileUtilities;
class RenameAction extends AbstractAction { class RenameAction extends AbstractAction {
@ -32,52 +31,32 @@ class RenameAction extends AbstractAction {
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
Deque<Match<File, File>> todoQueue = new ArrayDeque<Match<File, File>>(); List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
Deque<Match<File, File>> doneQueue = new ArrayDeque<Match<File, File>>();
for (Match<String, File> match : model.getMatchesForRenaming()) {
File source = match.getCandidate();
String extension = FileUtilities.getExtension(source);
StringBuilder name = new StringBuilder(match.getValue());
if (extension != null) {
name.append(".").append(extension);
}
// same parent, different name
File target = new File(source.getParentFile(), name.toString());
todoQueue.addLast(new Match<File, File>(source, target));
}
try { try {
int renameCount = todoQueue.size(); for (Entry<File, File> mapping : model.getRenameMap().entrySet()) {
for (Match<File, File> match : todoQueue) {
// rename file // rename file
if (!match.getValue().renameTo(match.getCandidate())) if (!mapping.getKey().renameTo(mapping.getValue()))
throw new IOException(String.format("Failed to rename file: %s.", match.getValue().getName())); throw new IOException(String.format("Failed to rename file: \"%s\".", mapping.getKey().getName()));
// revert in reverse order if renaming of all matches fails // remember successfully renamed matches for possible revert
doneQueue.addFirst(match); renameLog.add(mapping);
} }
// renamed all matches successfully // renamed all matches successfully
Logger.getLogger("ui").info(String.format("%d files renamed.", renameCount)); Logger.getLogger("ui").info(String.format("%d files renamed.", renameLog.size()));
} catch (IOException e) { } catch (Exception e) {
// rename failed // could not rename one of the files, revert all changes
Logger.getLogger("ui").warning(ExceptionUtilities.getRootCauseMessage(e)); Logger.getLogger("ui").warning(e.getMessage());
boolean revertSuccess = true; // revert in reverse order
Collections.reverse(renameLog);
// revert rename operations // revert rename operations
for (Match<File, File> match : doneQueue) { for (Entry<File, File> mapping : renameLog) {
revertSuccess &= match.getCandidate().renameTo(match.getValue()); if (!mapping.getValue().renameTo(mapping.getKey())) {
Logger.getLogger("ui").severe(String.format("Failed to revert file: \"%s\".", mapping.getValue().getName()));
} }
if (!revertSuccess) {
Logger.getLogger("ui").severe("Failed to revert all rename operations.");
} }
} }

View File

@ -2,6 +2,8 @@
package net.sourceforge.filebot.ui.panel.rename; package net.sourceforge.filebot.ui.panel.rename;
import static java.util.Collections.swap;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
@ -58,16 +60,6 @@ class RenameList<E> extends FileBotList<E> {
loadAction.putValue(LoadAction.TRANSFERABLE_POLICY, transferablePolicy); loadAction.putValue(LoadAction.TRANSFERABLE_POLICY, transferablePolicy);
} }
public void swap(int index1, int index2) {
E e1 = model.get(index1);
E e2 = model.get(index2);
// swap data
model.set(index1, e2);
model.set(index2, e1);
}
private final LoadAction loadAction = new LoadAction(null); private final LoadAction loadAction = new LoadAction(null);
private final AbstractAction upAction = new AbstractAction(null, ResourceManager.getIcon("action.up")) { private final AbstractAction upAction = new AbstractAction(null, ResourceManager.getIcon("action.up")) {
@ -76,7 +68,7 @@ class RenameList<E> extends FileBotList<E> {
int index = getListComponent().getSelectedIndex(); int index = getListComponent().getSelectedIndex();
if (index > 0) { if (index > 0) {
swap(index, index - 1); swap(model, index, index - 1);
getListComponent().setSelectedIndex(index - 1); getListComponent().setSelectedIndex(index - 1);
} }
} }
@ -88,7 +80,7 @@ class RenameList<E> extends FileBotList<E> {
int index = getListComponent().getSelectedIndex(); int index = getListComponent().getSelectedIndex();
if (index < model.size() - 1) { if (index < model.size() - 1) {
swap(index, index + 1); swap(model, index, index + 1);
getListComponent().setSelectedIndex(index + 1); getListComponent().setSelectedIndex(index + 1);
} }
} }
@ -109,8 +101,8 @@ class RenameList<E> extends FileBotList<E> {
public void mouseDragged(MouseEvent m) { public void mouseDragged(MouseEvent m) {
int currentIndex = getListComponent().getSelectedIndex(); int currentIndex = getListComponent().getSelectedIndex();
if (currentIndex != lastIndex) { if (currentIndex != lastIndex && lastIndex >= 0 && currentIndex >= 0) {
swap(lastIndex, currentIndex); swap(model, lastIndex, currentIndex);
lastIndex = currentIndex; lastIndex = currentIndex;
} }
} }

View File

@ -12,7 +12,7 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D; import java.awt.geom.RoundRectangle2D;
import java.io.File; import java.io.File;
import javax.swing.JLabel; import javax.swing.DefaultListCellRenderer;
import javax.swing.JList; import javax.swing.JList;
import javax.swing.border.CompoundBorder; import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
@ -22,13 +22,14 @@ import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture; import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture;
import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer; import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer;
import net.sourceforge.tuned.ui.GradientStyle;
class RenameListCellRenderer extends DefaultFancyListCellRenderer { class RenameListCellRenderer extends DefaultFancyListCellRenderer {
private final RenameModel renameModel; private final RenameModel renameModel;
private final TypeLabel typeLabel = new TypeLabel(); private final TypeRenderer typeRenderer = new TypeRenderer();
private final Color noMatchGradientBeginColor = new Color(0xB7B7B7); private final Color noMatchGradientBeginColor = new Color(0xB7B7B7);
private final Color noMatchGradientEndColor = new Color(0x9A9A9A); private final Color noMatchGradientEndColor = new Color(0x9A9A9A);
@ -39,8 +40,8 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
setHighlightingEnabled(false); setHighlightingEnabled(false);
setLayout(new MigLayout("fill, insets 0", "align left", "align center")); setLayout(new MigLayout("insets 0, fill", "align left", "align center"));
add(typeLabel, "gap rel:push"); add(typeRenderer, "gap rel:push, hidemode 3");
} }
@ -48,19 +49,24 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
public void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { public void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus); super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
// reset // reset decoration
setIcon(null); setIcon(null);
typeLabel.setText(null); typeRenderer.setVisible(false);
typeLabel.setAlpha(1.0f); typeRenderer.setAlpha(1.0f);
if (value instanceof File) { if (value instanceof File) {
// display file extension // display file extension
File file = (File) value; File file = (File) value;
if (renameModel.preserveExtension()) {
setText(FileUtilities.getName(file)); setText(FileUtilities.getName(file));
typeLabel.setText(getType(file)); typeRenderer.setText(getType(file));
typeRenderer.setVisible(true);
} else {
setText(file.getName());
}
} else if (value instanceof FormattedFuture) { } else if (value instanceof FormattedFuture) {
// progress icon and value type // display progress icon
FormattedFuture future = (FormattedFuture) value; FormattedFuture future = (FormattedFuture) value;
switch (future.getState()) { switch (future.getState()) {
@ -78,7 +84,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
setGradientColors(noMatchGradientBeginColor, noMatchGradientEndColor); setGradientColors(noMatchGradientBeginColor, noMatchGradientEndColor);
} else { } else {
setForeground(noMatchGradientBeginColor); setForeground(noMatchGradientBeginColor);
typeLabel.setAlpha(0.5f); typeRenderer.setAlpha(0.5f);
} }
} }
} }
@ -98,7 +104,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
} }
private class TypeLabel extends JLabel { private static class TypeRenderer extends DefaultListCellRenderer {
private final Insets margin = new Insets(0, 10, 0, 0); private final Insets margin = new Insets(0, 10, 0, 0);
private final Insets padding = new Insets(0, 6, 0, 5); private final Insets padding = new Insets(0, 6, 0, 5);
@ -110,7 +116,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
private float alpha = 1.0f; private float alpha = 1.0f;
public TypeLabel() { public TypeRenderer() {
setOpaque(false); setOpaque(false);
setForeground(new Color(0x141414)); setForeground(new Color(0x141414));
@ -128,7 +134,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
g2d.setComposite(AlphaComposite.SrcOver.derive(alpha)); g2d.setComposite(AlphaComposite.SrcOver.derive(alpha));
g2d.setPaint(getGradientStyle().getGradientPaint(shape, gradientBeginColor, gradientEndColor)); g2d.setPaint(GradientStyle.TOP_TO_BOTTOM.getGradientPaint(shape, gradientBeginColor, gradientEndColor));
g2d.fill(shape); g2d.fill(shape);
g2d.setFont(getFont()); g2d.setFont(getFont());
@ -139,15 +145,6 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
} }
@Override
public void setText(String text) {
super.setText(text);
// auto-hide if text is null
setVisible(text != null);
}
public void setAlpha(float alpha) { public void setAlpha(float alpha) {
this.alpha = alpha; this.alpha = alpha;
} }

View File

@ -7,6 +7,7 @@ import java.beans.PropertyChangeListener;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -20,6 +21,7 @@ import javax.swing.SwingWorker;
import javax.swing.SwingWorker.StateValue; import javax.swing.SwingWorker.StateValue;
import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.ui.TunedUtilities; import net.sourceforge.tuned.ui.TunedUtilities;
import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList; import ca.odell.glazedlists.TransformedList;
@ -52,17 +54,7 @@ public class RenameModel extends MatchModel<Object, File> {
} }
}; };
private boolean preserveExtension = true;
public void useFormatter(Class<?> type, MatchFormatter formatter) {
if (formatter != null) {
formatters.put(type, formatter);
} else {
formatters.remove(type);
}
// reformat matches
names.refresh();
}
public EventList<FormattedFuture> names() { public EventList<FormattedFuture> names() {
@ -75,16 +67,62 @@ public class RenameModel extends MatchModel<Object, File> {
} }
public List<Match<String, File>> getMatchesForRenaming() { public boolean preserveExtension() {
List<Match<String, File>> matches = new ArrayList<Match<String, File>>(); return preserveExtension;
}
for (int i = 0; i < size(); i++) {
if (hasComplement(i) && names.get(i).isDone()) { public void setPreserveExtension(boolean preserveExtension) {
matches.add(new Match<String, File>(names().get(i).toString(), files().get(i))); this.preserveExtension = preserveExtension;
}
public Map<File, File> getRenameMap() {
Map<File, File> map = new LinkedHashMap<File, File>();
for (int i = 0; i < names.size(); i++) {
if (hasComplement(i)) {
FormattedFuture future = names.get(i);
// check if background formatter is done
if (!future.isDone()) {
throw new IllegalStateException(String.format("\"%s\" has not been formatted yet.", future.toString()));
}
File originalFile = files().get(i);
StringBuilder newName = new StringBuilder(future.toString());
if (preserveExtension) {
String extension = FileUtilities.getExtension(originalFile);
if (extension != null) {
newName.append(".").append(extension);
} }
} }
return matches; // same parent, different name
File newFile = new File(originalFile.getParentFile(), newName.toString());
// insert mapping
if (map.put(originalFile, newFile) != null) {
throw new IllegalStateException(String.format("Duplicate file entry: \"%s\"", originalFile.getName()));
}
}
}
return map;
}
public void useFormatter(Class<?> type, MatchFormatter formatter) {
if (formatter != null) {
formatters.put(type, formatter);
} else {
formatters.remove(type);
}
// reformat matches
names.refresh();
} }

View File

@ -23,11 +23,9 @@ import java.util.prefs.Preferences;
import javax.script.ScriptException; import javax.script.ScriptException;
import javax.swing.AbstractAction; import javax.swing.AbstractAction;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants; import javax.swing.SwingConstants;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
@ -53,8 +51,8 @@ import net.sourceforge.tuned.PreferencesMap.AbstractAdapter;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
import net.sourceforge.tuned.ui.ActionPopup; import net.sourceforge.tuned.ui.ActionPopup;
import net.sourceforge.tuned.ui.LoadingOverlayPane; import net.sourceforge.tuned.ui.LoadingOverlayPane;
import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.ListSelection;
import ca.odell.glazedlists.event.ListEventListener; import ca.odell.glazedlists.swing.EventSelectionModel;
public class RenamePanel extends JComponent { public class RenamePanel extends JComponent {
@ -88,8 +86,8 @@ public class RenamePanel extends JComponent {
namesList.getListComponent().setCellRenderer(cellrenderer); namesList.getListComponent().setCellRenderer(cellrenderer);
filesList.getListComponent().setCellRenderer(cellrenderer); filesList.getListComponent().setCellRenderer(cellrenderer);
ListSelectionModel selectionModel = new DefaultListSelectionModel(); EventSelectionModel<Match<Object, File>> selectionModel = new EventSelectionModel<Match<Object, File>>(renameModel.matches());
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); selectionModel.setSelectionMode(ListSelection.MULTIPLE_INTERVAL_SELECTION_DEFENSIVE);
// use the same selection model for both lists to synchronize selection // use the same selection model for both lists to synchronize selection
namesList.getListComponent().setSelectionModel(selectionModel); namesList.getListComponent().setSelectionModel(selectionModel);
@ -127,10 +125,6 @@ public class RenamePanel extends JComponent {
add(renameButton, "gapy 30px, sizegroupx button"); add(renameButton, "gapy 30px, sizegroupx button");
add(new LoadingOverlayPane(namesList, namesList, "28px", "30px"), "grow, sizegroupx list"); add(new LoadingOverlayPane(namesList, namesList, "28px", "30px"), "grow, sizegroupx list");
// repaint on change
renameModel.names().addListEventListener(new RepaintHandler<Object>());
renameModel.files().addListEventListener(new RepaintHandler<Object>());
} }
@ -240,7 +234,7 @@ public class RenamePanel extends JComponent {
// add remaining file entries // add remaining file entries
renameModel.files().addAll(remainingFiles()); renameModel.files().addAll(remainingFiles());
} catch (Exception e) { } catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e); Logger.getLogger("ui").warning(ExceptionUtilities.getRootCauseMessage(e));
} finally { } finally {
// auto-match finished // auto-match finished
namesList.firePropertyChange(LOADING_PROPERTY, true, false); namesList.firePropertyChange(LOADING_PROPERTY, true, false);
@ -278,7 +272,7 @@ public class RenamePanel extends JComponent {
// multiple results have been found, user must select one // multiple results have been found, user must select one
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(SwingUtilities.getWindowAncestor(RenamePanel.this), probableMatches.isEmpty() ? searchResults : probableMatches); SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(SwingUtilities.getWindowAncestor(RenamePanel.this), probableMatches.isEmpty() ? searchResults : probableMatches);
selectDialog.getHeaderLabel().setText(String.format("Shows matching '%s':", query)); selectDialog.getHeaderLabel().setText(String.format("Shows matching \"%s\":", query));
selectDialog.setVisible(true); selectDialog.setVisible(true);
@ -299,17 +293,6 @@ public class RenamePanel extends JComponent {
} }
} }
protected class RepaintHandler<E> implements ListEventListener<E> {
@Override
public void listChanged(ListEvent<E> listChanges) {
namesList.repaint();
filesList.repaint();
}
};
protected final PreferencesEntry<EpisodeExpressionFormatter> persistentFormatExpression = Settings.userRoot().entry("rename.format", new AbstractAdapter<EpisodeExpressionFormatter>() { protected final PreferencesEntry<EpisodeExpressionFormatter> persistentFormatExpression = Settings.userRoot().entry("rename.format", new AbstractAdapter<EpisodeExpressionFormatter>() {
@Override @Override

View File

@ -12,6 +12,7 @@ import java.util.concurrent.CancellationException;
import javax.swing.SwingWorker.StateValue; import javax.swing.SwingWorker.StateValue;
import javax.swing.event.SwingPropertyChangeSupport; import javax.swing.event.SwingPropertyChangeSupport;
import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;

View File

@ -12,6 +12,9 @@ import java.util.concurrent.CancellationException;
import javax.swing.SwingWorker; import javax.swing.SwingWorker;
import net.sourceforge.filebot.hash.Hash;
import net.sourceforge.filebot.hash.HashType;
class ChecksumComputationTask extends SwingWorker<Map<HashType, String>, Void> { class ChecksumComputationTask extends SwingWorker<Map<HashType, String>, Void> {

View File

@ -16,6 +16,7 @@ import java.util.Set;
import javax.swing.event.SwingPropertyChangeSupport; import javax.swing.event.SwingPropertyChangeSupport;
import net.sourceforge.filebot.FileBotUtilities; import net.sourceforge.filebot.FileBotUtilities;
import net.sourceforge.filebot.hash.HashType;
class ChecksumRow { class ChecksumRow {

View File

@ -9,6 +9,8 @@ import java.util.Date;
import java.util.Formatter; import java.util.Formatter;
import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.hash.VerificationFilePrinter;
import net.sourceforge.filebot.ui.transfer.TextFileExportHandler; import net.sourceforge.filebot.ui.transfer.TextFileExportHandler;
import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities;

View File

@ -18,6 +18,7 @@ import java.util.Set;
import javax.swing.table.AbstractTableModel; import javax.swing.table.AbstractTableModel;
import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities;

View File

@ -15,7 +15,9 @@ import java.util.concurrent.ExecutorService;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScanner.IllegalSyntaxException; import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.hash.IllegalSyntaxException;
import net.sourceforge.filebot.hash.VerificationFileScanner;
import net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy; import net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter; import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter;

View File

@ -30,6 +30,7 @@ import javax.swing.border.TitledBorder;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler;
import net.sourceforge.filebot.ui.transfer.LoadAction; import net.sourceforge.filebot.ui.transfer.LoadAction;

View File

@ -26,7 +26,10 @@ public class DefaultThreadFactory implements ThreadFactory {
public DefaultThreadFactory(String groupName, int priority, boolean daemon) { public DefaultThreadFactory(String groupName, int priority, boolean daemon) {
group = new ThreadGroup(groupName); SecurityManager sm = System.getSecurityManager();
ThreadGroup parentGroup = (sm != null) ? sm.getThreadGroup() : Thread.currentThread().getThreadGroup();
this.group = new ThreadGroup(parentGroup, groupName);
this.daemon = daemon; this.daemon = daemon;
this.priority = priority; this.priority = priority;
@ -45,4 +48,9 @@ public class DefaultThreadFactory implements ThreadFactory {
return thread; return thread;
} }
public ThreadGroup getThreadGroup() {
return group;
}
} }

View File

@ -13,6 +13,19 @@ public final class ExceptionUtilities {
} }
@SuppressWarnings("unchecked")
public static <T extends Throwable> T findCause(Throwable t, Class<T> type) {
while (t != null) {
if (type.isInstance(t))
return (T) t;
t = t.getCause();
}
return null;
}
public static String getRootCauseMessage(Throwable t) { public static String getRootCauseMessage(Throwable t) {
return getMessage(getRootCause(t)); return getMessage(getRootCause(t));
} }
@ -22,7 +35,7 @@ public final class ExceptionUtilities {
String message = t.getMessage(); String message = t.getMessage();
if (message == null || message.isEmpty()) { if (message == null || message.isEmpty()) {
message = t.toString().replaceAll(t.getClass().getName(), t.getClass().getSimpleName()); message = t.toString();
} }
return message; return message;

View File

@ -5,6 +5,7 @@ package net.sourceforge.tuned.ui;
import java.awt.Color; import java.awt.Color;
import java.awt.Insets; import java.awt.Insets;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JList; import javax.swing.JList;
@ -12,7 +13,7 @@ import javax.swing.JList;
public class DefaultFancyListCellRenderer extends AbstractFancyListCellRenderer { public class DefaultFancyListCellRenderer extends AbstractFancyListCellRenderer {
private final JLabel label = new JLabel(); private final JLabel label = new DefaultListCellRenderer();
public DefaultFancyListCellRenderer() { public DefaultFancyListCellRenderer() {

View File

@ -3,9 +3,9 @@ package net.sourceforge.filebot;
import net.sourceforge.filebot.format.ExpressionFormatTest; import net.sourceforge.filebot.format.ExpressionFormatTest;
import net.sourceforge.filebot.hash.VerificationFileScannerTest;
import net.sourceforge.filebot.similarity.SimilarityTestSuite; import net.sourceforge.filebot.similarity.SimilarityTestSuite;
import net.sourceforge.filebot.ui.panel.rename.MatchModelTest; import net.sourceforge.filebot.ui.panel.rename.MatchModelTest;
import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScannerTest;
import net.sourceforge.filebot.web.WebTestSuite; import net.sourceforge.filebot.web.WebTestSuite;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;

View File

@ -1,8 +1,9 @@
package net.sourceforge.filebot.ui.panel.sfv; package net.sourceforge.filebot.hash;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.io.File; import java.io.File;
import java.util.Scanner; import java.util.Scanner;