diff --git a/source/ehcache.xml b/source/ehcache.xml index 9b7505a2..950d798a 100644 --- a/source/ehcache.xml +++ b/source/ehcache.xml @@ -106,7 +106,7 @@ arguments() { return arguments; } diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index 720e9d7c..812b5875 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -4,6 +4,12 @@ package net.sourceforge.filebot; 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.Level; import java.util.logging.Logger; @@ -12,10 +18,10 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.UIManager; +import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.ui.MainFrame; import net.sourceforge.filebot.ui.NotificationLoggingHandler; import net.sourceforge.filebot.ui.SinglePanelFrame; -import net.sourceforge.filebot.ui.panel.analyze.AnalyzePanelBuilder; import net.sourceforge.filebot.ui.panel.sfv.SfvPanelBuilder; import org.kohsuke.args4j.CmdLineException; @@ -45,6 +51,7 @@ public class Main { initializeLogging(); initializeSettings(); + initializeSecurityManager(); try { // use native laf an all platforms @@ -59,19 +66,18 @@ public class Main { public void run() { JFrame frame; - if (argumentBean.analyze()) { - frame = new SinglePanelFrame(new AnalyzePanelBuilder()).publish(argumentBean.transferable()); - } else if (argumentBean.sfv()) { + if (argumentBean.sfv()) { + // sfv frame frame = new SinglePanelFrame(new SfvPanelBuilder()).publish(argumentBean.transferable()); } else { - // default + // default frame frame = new MainFrame(); } frame.setLocationByPlatform(true); frame.setDefaultCloseOperation(EXIT_ON_CLOSE); - // start + // start application frame.setVisible(true); } }); @@ -95,11 +101,51 @@ public class Main { } + /** + * Preset the default thetvdb.apikey. + */ private static void initializeSettings() { 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 { ArgumentBean argumentBean = new ArgumentBean(); @@ -109,6 +155,9 @@ public class Main { } + /** + * Print command line argument usage. + */ private static void printUsage(ArgumentBean argumentBean) { System.out.println("Options:"); diff --git a/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java b/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java index 5d5a65e8..2746b0b2 100644 --- a/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java +++ b/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java @@ -2,6 +2,7 @@ package net.sourceforge.filebot.format; +import static net.sourceforge.filebot.FileBotUtilities.SFV_FILES; import static net.sourceforge.filebot.format.Define.undefined; import java.io.File; @@ -9,15 +10,21 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; 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 net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; 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.StreamKind; import net.sourceforge.filebot.web.Episode; +import net.sourceforge.tuned.FileUtilities; 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") public String getHeightAndInterlacement() { 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") public String getVideoResolution() { String width = getMediaInfo(StreamKind.Video, 0, "Width"); @@ -117,11 +125,16 @@ public class EpisodeFormatBindingBean { public String getCRC32() throws IOException, InterruptedException { if (mediaFile != null) { // try to get checksum from file name - String embeddedChecksum = FileBotUtilities.getEmbeddedChecksum(mediaFile.getName()); + String checksum = FileBotUtilities.getEmbeddedChecksum(mediaFile.getName()); - if (embeddedChecksum != null) { - return embeddedChecksum; - } + if (checksum != null) + return checksum; + + // try to get checksum from sfv file + checksum = getChecksumFromSfvFile(mediaFile); + + if (checksum != null) + return checksum; // calculate checksum from file return crc32(mediaFile); @@ -131,6 +144,13 @@ public class EpisodeFormatBindingBean { } + @Define("ext") + public String getContainerExtension() { + // file extension + return FileUtilities.getExtension(mediaFile); + } + + @Define("general") public Object getGeneralMediaInfo() { return new AssociativeScriptObject(getMediaInfo().snapshot(StreamKind.General, 0)); @@ -161,11 +181,13 @@ public class EpisodeFormatBindingBean { } + @Define("episode") public Episode getEpisode() { return episode; } + @Define("file") public File getMediaFile() { 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 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 { // try to get checksum from cache Cache cache = CacheManager.getInstance().getCache("checksum"); diff --git a/source/net/sourceforge/filebot/format/ExpressionBindings.java b/source/net/sourceforge/filebot/format/ExpressionBindings.java index 9e01fad3..644c4bd6 100644 --- a/source/net/sourceforge/filebot/format/ExpressionBindings.java +++ b/source/net/sourceforge/filebot/format/ExpressionBindings.java @@ -16,16 +16,16 @@ import net.sourceforge.tuned.ExceptionUtilities; public class ExpressionBindings extends AbstractMap implements Bindings { - protected final Object bean; + protected final Object bindingBean; protected final Map bindings = new HashMap(); public ExpressionBindings(Object bindingBean) { - bean = bindingBean; + this.bindingBean = bindingBean; // get method bindings - for (Method method : bean.getClass().getMethods()) { + for (Method method : bindingBean.getClass().getMethods()) { Define define = method.getAnnotation(Define.class); if (define != null) { @@ -41,19 +41,19 @@ public class ExpressionBindings extends AbstractMap implements B public Object getBindingBean() { - return bean; + return bindingBean; } - protected Object evaluate(Method method) throws Exception { - Object value = method.invoke(getBindingBean()); + protected Object evaluate(final Method method) throws Exception { + Object value = method.invoke(bindingBean); if (value != null) { return value; } // invoke fallback method - return bindings.get(Define.undefined).invoke(getBindingBean()); + return bindings.get(Define.undefined).invoke(bindingBean); } diff --git a/source/net/sourceforge/filebot/format/ExpressionException.java b/source/net/sourceforge/filebot/format/ExpressionException.java new file mode 100644 index 00000000..0ae8e41c --- /dev/null +++ b/source/net/sourceforge/filebot/format/ExpressionException.java @@ -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; + } + +} diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.java b/source/net/sourceforge/filebot/format/ExpressionFormat.java index 4be3b198..a6fbe3f2 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.java +++ b/source/net/sourceforge/filebot/format/ExpressionFormat.java @@ -2,12 +2,26 @@ package net.sourceforge.filebot.format; +import java.io.FilePermission; 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.Format; import java.text.ParsePosition; import java.util.ArrayList; import java.util.List; +import java.util.PropertyPermission; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -19,6 +33,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptException; import javax.script.SimpleScriptContext; +import net.sourceforge.tuned.ExceptionUtilities; + +import org.mozilla.javascript.EcmaError; + import com.sun.phobos.script.javascript.RhinoScriptEngine; @@ -33,7 +51,7 @@ public class ExpressionFormat extends Format { public ExpressionFormat(String expression) throws ScriptException { 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) { 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) { if (snipped instanceof CompiledScript) { @@ -108,9 +128,16 @@ public class ExpressionFormat extends Format { sb.append(value); } } catch (ScriptException e) { - lastException = e; - } catch (Exception e) { - lastException = new 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; + } + } catch (RuntimeException e) { + lastException = new ExpressionException(e); } } else { 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("<>", "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() { + + @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() { + + @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 public Object parseObject(String source, ParsePosition pos) { throw new UnsupportedOperationException(); diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumHash.java b/source/net/sourceforge/filebot/hash/ChecksumHash.java similarity index 81% rename from source/net/sourceforge/filebot/ui/panel/sfv/ChecksumHash.java rename to source/net/sourceforge/filebot/hash/ChecksumHash.java index e99ae238..5fe6dc96 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumHash.java +++ b/source/net/sourceforge/filebot/hash/ChecksumHash.java @@ -1,11 +1,11 @@ -package net.sourceforge.filebot.ui.panel.sfv; +package net.sourceforge.filebot.hash; import java.util.zip.Checksum; -class ChecksumHash implements Hash { +public class ChecksumHash implements Hash { private final Checksum checksum; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/Hash.java b/source/net/sourceforge/filebot/hash/Hash.java similarity index 58% rename from source/net/sourceforge/filebot/ui/panel/sfv/Hash.java rename to source/net/sourceforge/filebot/hash/Hash.java index 51022557..b27c2c70 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/Hash.java +++ b/source/net/sourceforge/filebot/hash/Hash.java @@ -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); diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/HashType.java b/source/net/sourceforge/filebot/hash/HashType.java similarity index 58% rename from source/net/sourceforge/filebot/ui/panel/sfv/HashType.java rename to source/net/sourceforge/filebot/hash/HashType.java index c721f8d4..2a94f54f 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/HashType.java +++ b/source/net/sourceforge/filebot/hash/HashType.java @@ -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.Scanner; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.zip.CRC32; -enum HashType { +public enum HashType { SFV { @@ -23,44 +19,13 @@ enum HashType { @Override public VerificationFileScanner newScanner(Scanner scanner) { - // adapt default scanner to sfv line syntax - return new VerificationFileScanner(scanner) { - - /** - * Pattern used to parse the lines of a sfv file. - * - *
-				 * Sample:
-				 * folder/file.txt 970E4EF1
-				 * |  Group 1    | | Gr.2 |
-				 * 
- */ - private final Pattern pattern = Pattern.compile("(.+)\\s+(\\p{XDigit}{8})"); - - - @Override - protected Entry 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)); - } - }; + return new SfvFileScanner(scanner); } @Override public VerificationFilePrinter newPrinter(Formatter out) { - return new VerificationFilePrinter(out, "CRC32") { - - @Override - public void print(String path, String hash) { - // e.g folder/file.txt 970E4EF1 - out.format(String.format("%s %s", path, hash)); - } - }; + return new SfvFilePrinter(out); } }, diff --git a/source/net/sourceforge/filebot/hash/IllegalSyntaxException.java b/source/net/sourceforge/filebot/hash/IllegalSyntaxException.java new file mode 100644 index 00000000..30dc83c3 --- /dev/null +++ b/source/net/sourceforge/filebot/hash/IllegalSyntaxException.java @@ -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); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/MessageDigestHash.java b/source/net/sourceforge/filebot/hash/MessageDigestHash.java similarity index 88% rename from source/net/sourceforge/filebot/ui/panel/sfv/MessageDigestHash.java rename to source/net/sourceforge/filebot/hash/MessageDigestHash.java index b6b1930b..3e18e59f 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/MessageDigestHash.java +++ b/source/net/sourceforge/filebot/hash/MessageDigestHash.java @@ -1,5 +1,5 @@ -package net.sourceforge.filebot.ui.panel.sfv; +package net.sourceforge.filebot.hash; import java.math.BigInteger; @@ -7,7 +7,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -class MessageDigestHash implements Hash { +public class MessageDigestHash implements Hash { private final MessageDigest md; diff --git a/source/net/sourceforge/filebot/hash/SfvFilePrinter.java b/source/net/sourceforge/filebot/hash/SfvFilePrinter.java new file mode 100644 index 00000000..2a7913b1 --- /dev/null +++ b/source/net/sourceforge/filebot/hash/SfvFilePrinter.java @@ -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)); + } +} diff --git a/source/net/sourceforge/filebot/hash/SfvFileScanner.java b/source/net/sourceforge/filebot/hash/SfvFileScanner.java new file mode 100644 index 00000000..034eb407 --- /dev/null +++ b/source/net/sourceforge/filebot/hash/SfvFileScanner.java @@ -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. + * + *
+	 * Sample:
+	 * folder/file.txt 970E4EF1
+	 * |  Group 1    | | Gr.2 |
+	 * 
+ */ + private final Pattern pattern = Pattern.compile("(.+)\\s+(\\p{XDigit}{8})"); + + + @Override + protected Entry 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)); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/VerificationFilePrinter.java b/source/net/sourceforge/filebot/hash/VerificationFilePrinter.java similarity index 58% rename from source/net/sourceforge/filebot/ui/panel/sfv/VerificationFilePrinter.java rename to source/net/sourceforge/filebot/hash/VerificationFilePrinter.java index ed2a4b5a..4f696456 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/VerificationFilePrinter.java +++ b/source/net/sourceforge/filebot/hash/VerificationFilePrinter.java @@ -1,5 +1,5 @@ -package net.sourceforge.filebot.ui.panel.sfv; +package net.sourceforge.filebot.hash; import java.io.Closeable; @@ -7,7 +7,7 @@ import java.io.IOException; import java.util.Formatter; -class VerificationFilePrinter implements Closeable { +public class VerificationFilePrinter implements Closeable { protected final Formatter out; protected final String algorithm; @@ -20,17 +20,8 @@ class VerificationFilePrinter implements Closeable { public void println(String path, String hash) { - // print entry - print(path, hash); - - // print line separator - out.format("%n"); - } - - - protected void print(String path, String hash) { // e.g. 1a02a7c1e9ac91346d08829d5037b240f42ded07 ?SHA1*folder/file.txt - out.format("%s %s*%s", hash, algorithm == null ? "" : '?' + algorithm.toUpperCase(), path); + out.format("%s %s*%s%n", hash, algorithm == null ? "" : '?' + algorithm.toUpperCase(), path); } diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/VerificationFileScanner.java b/source/net/sourceforge/filebot/hash/VerificationFileScanner.java similarity index 79% rename from source/net/sourceforge/filebot/ui/panel/sfv/VerificationFileScanner.java rename to source/net/sourceforge/filebot/hash/VerificationFileScanner.java index cf1645ac..ab31931f 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/VerificationFileScanner.java +++ b/source/net/sourceforge/filebot/hash/VerificationFileScanner.java @@ -1,5 +1,5 @@ -package net.sourceforge.filebot.ui.panel.sfv; +package net.sourceforge.filebot.hash; import java.io.Closeable; @@ -16,7 +16,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -class VerificationFileScanner implements Iterator>, Closeable { +public class VerificationFileScanner implements Iterator>, Closeable { private final Scanner scanner; @@ -48,7 +48,7 @@ class VerificationFileScanner implements Iterator>, Closeabl @Override - public Entry next() { + public Entry next() throws IllegalSyntaxException { // cache next line if (!hasNext()) { throw new NoSuchElementException(); @@ -88,10 +88,10 @@ class VerificationFileScanner implements Iterator>, Closeabl * | Group 1 | | Group 2 | * */ - private final Pattern pattern = Pattern.compile("(\\p{XDigit}{8,})\\s+(?:\\?\\w+)?\\*(.+)"); + private final Pattern pattern = Pattern.compile("(\\p{XDigit}+)\\s+(?:\\?\\w+)?\\*(.+)"); - protected Entry parseLine(String line) { + protected Entry parseLine(String line) throws IllegalSyntaxException { Matcher matcher = pattern.matcher(line); if (!matcher.matches()) @@ -127,18 +127,4 @@ class VerificationFileScanner implements Iterator>, Closeabl 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); - } - - } - } diff --git a/source/net/sourceforge/filebot/torrent/Torrent.java b/source/net/sourceforge/filebot/torrent/Torrent.java index 9bb2e6fb..dea73e1a 100644 --- a/source/net/sourceforge/filebot/torrent/Torrent.java +++ b/source/net/sourceforge/filebot/torrent/Torrent.java @@ -2,11 +2,11 @@ package net.sourceforge.filebot.torrent; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; +import java.io.InputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; @@ -16,8 +16,6 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import net.sourceforge.tuned.ByteBufferInputStream; - public class Torrent { @@ -107,13 +105,12 @@ public class Torrent { private static Map decodeTorrent(File torrent) throws IOException { - FileChannel fileChannel = new FileInputStream(torrent).getChannel(); + InputStream in = new BufferedInputStream(new FileInputStream(torrent)); try { - // memory-map and decode torrent - return BDecoder.decode(new ByteBufferInputStream(fileChannel.map(MapMode.READ_ONLY, 0, fileChannel.size()))); + return BDecoder.decode(in); } finally { - fileChannel.close(); + in.close(); } } @@ -206,6 +203,12 @@ public class Torrent { public String getPath() { return path; } + + + @Override + public String toString() { + return getPath(); + } } } diff --git a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java index c6757ede..5fa52467 100644 --- a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java +++ b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java @@ -17,7 +17,9 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; 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.concurrent.ArrayBlockingQueue; 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.web.Episode; import net.sourceforge.filebot.web.Episode.EpisodeFormat; +import net.sourceforge.tuned.DefaultThreadFactory; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ui.GradientStyle; import net.sourceforge.tuned.ui.LinkButton; @@ -68,8 +71,7 @@ public class EpisodeFormatDialog extends JDialog { private JLabel preview = new JLabel(); - private JLabel warningMessage = new JLabel(ResourceManager.getIcon("status.warning")); - private JLabel errorMessage = new JLabel(ResourceManager.getIcon("status.error")); + private JLabel status = new JLabel(); private EpisodeFormatBindingBean previewSample = new EpisodeFormatBindingBean(getPreviewSampleEpisode(), getPreviewSampleMediaFile()); @@ -107,15 +109,10 @@ public class EpisodeFormatDialog extends JDialog { header.setBackground(Color.white); 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(title, "wrap unrel:push"); - header.add(preview, "gap indent, hidemode 3, wmax 90%"); - header.add(errorMessage, "gap indent, hidemode 3, wmax 90%, newline"); - header.add(warningMessage, "gap indent, hidemode 3, wmax 90%, newline"); + header.add(preview, "hmin 16px, gap indent, hidemode 3, wmax 90%"); + header.add(status, "hmin 16px, gap indent, hidemode 3, wmax 90%, newline"); 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(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(approveFormatAction), "tag apply"); @@ -137,12 +134,8 @@ public class EpisodeFormatDialog extends JDialog { pane.add(header, "h 60px, growx, dock north"); pane.add(content, "grow"); - setSize(485, 415); - header.setComponentPopupMenu(createPreviewSamplePopup()); - setLocation(TunedUtilities.getPreferredLocation(this)); - // update format on change editor.getDocument().addDocumentListener(new LazyDocumentAdapter() { @@ -171,6 +164,10 @@ public class EpisodeFormatDialog extends JDialog { // update preview to current format 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")); panel.setBorder(new LineBorder(new Color(0xACA899))); panel.setBackground(new Color(0xFFFFE1)); - panel.setOpaque(true); ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName()); - // sort keys - String[] keys = bundle.keySet().toArray(new String[0]); - Arrays.sort(keys); + // collect example keys + List examples = new ArrayList(); - for (String key : keys) { - if (key.startsWith("example")) { - String format = bundle.getString(key); + for (String key : bundle.keySet()) { + if (key.startsWith("example")) + 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) { - LinkButton formatLink = new LinkButton(new ExampleFormatAction(format)); - formatLink.setFont(new Font(MONOSPACED, PLAIN, 11)); + @Override + public void actionPerformed(ActionEvent e) { + editor.setText(format); + } + }); + + formatLink.setFont(new Font(MONOSPACED, PLAIN, 11)); + + final JLabel formatExample = new JLabel(); + + // bind text to preview + addPropertyChangeListener("previewSample", new PropertyChangeListener() { - panel.add(formatLink); - panel.add(new JLabel("...")); - panel.add(new ExampleFormatLabel(format)); - } + @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(new JLabel("...")); + panel.add(formatExample); } return panel; @@ -307,7 +332,31 @@ public class EpisodeFormatDialog extends JDialog { private ExecutorService createPreviewExecutor() { - ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue(1)); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue(1), new DefaultThreadFactory("PreviewFormatter")) { + + @SuppressWarnings("deprecation") + @Override + public List shutdownNow() { + List 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 executor.setRejectedExecutionHandler(new DiscardOldestPolicy()); @@ -345,34 +394,33 @@ public class EpisodeFormatDialog extends JDialog { // check internal script exception and empty output if (format.scriptException() != null) { - warningMessage.setText(format.scriptException().getCause().getMessage()); + throw format.scriptException(); } else if (get().trim().isEmpty()) { - warningMessage.setText("Formatted value is empty"); - } else { - warningMessage.setText(null); + throw new RuntimeException("Formatted value is empty"); } + + // no warning or error + status.setVisible(false); } 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); + editor.setForeground(defaultColor); + + progressIndicatorTimer.stop(); + progressIndicator.setVisible(false); } - - preview.setVisible(true); - warningMessage.setVisible(warningMessage.getText() != null); - errorMessage.setVisible(false); - - editor.setForeground(defaultColor); - - progressIndicatorTimer.stop(); - progressIndicator.setVisible(false); } }); } catch (ScriptException e) { // incorrect syntax - errorMessage.setText(ExceptionUtilities.getRootCauseMessage(e)); - errorMessage.setVisible(true); + status.setText(ExceptionUtilities.getRootCauseMessage(e)); + status.setIcon(ResourceManager.getIcon("status.error")); + status.setVisible(true); preview.setVisible(false); - warningMessage.setVisible(false); - editor.setForeground(errorColor); } } @@ -418,6 +466,9 @@ public class EpisodeFormatDialog extends JDialog { @Override public void actionPerformed(ActionEvent evt) { try { + if (progressIndicator.isVisible()) + throw new IllegalStateException("Format has not been verified yet."); + // check syntax new ExpressionFormat(editor.getText()); @@ -425,8 +476,8 @@ public class EpisodeFormatDialog extends JDialog { Settings.userRoot().put("dialog.format", editor.getText()); finish(Option.APPROVE); - } catch (ScriptException e) { - Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e); + } catch (Exception 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 { private final Timer timer = new Timer(200, new ActionListener() { diff --git a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties index ac3ec5e2..e3c8dd49 100644 --- a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties +++ b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties @@ -3,11 +3,11 @@ syntax: { } ... expression, n ... name, s ... # basic 1.01 example[0]: {n} - {s}.{e} - {t} -# 1x01 -example[1]: {n} - {s+'x'}{e.pad(2)} - # 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 -example[3]: {n.space('.').toLowerCase()} +example[3]: {n.space('.').toLowerCase()}.{s}{e.pad(2)} \ No newline at end of file diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java index 8b9f0897..c56771a0 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java @@ -51,7 +51,7 @@ class MatchAction extends AbstractAction { this.model = model; this.metrics = createMetrics(); - putValue(SHORT_DESCRIPTION, "Match names to files"); + putValue(SHORT_DESCRIPTION, "Match files and names"); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java index bf86a1d5..821e5493 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java @@ -15,7 +15,7 @@ import ca.odell.glazedlists.TransformedList; import ca.odell.glazedlists.event.ListEvent; -class MatchModel { +public class MatchModel { private final EventList> source = new BasicEventList>(); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java index a94c5ce1..a4bbee8b 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java @@ -28,17 +28,15 @@ import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy; import net.sourceforge.filebot.web.Episode; import net.sourceforge.tuned.FastFile; -import ca.odell.glazedlists.EventList; - class NamesListTransferablePolicy extends FileTransferablePolicy { private static final DataFlavor episodeArrayFlavor = ArrayTransferable.flavor(Episode.class); - private final EventList model; + private final List model; - public NamesListTransferablePolicy(EventList model) { + public NamesListTransferablePolicy(List model) { this.model = model; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java index f1d4cf99..9c3c187a 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java @@ -5,16 +5,15 @@ package net.sourceforge.filebot.ui.panel.rename; import java.awt.event.ActionEvent; import java.io.File; import java.io.IOException; -import java.util.ArrayDeque; -import java.util.Deque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; import java.util.logging.Logger; import javax.swing.AbstractAction; 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 { @@ -32,52 +31,32 @@ class RenameAction extends AbstractAction { public void actionPerformed(ActionEvent evt) { - Deque> todoQueue = new ArrayDeque>(); - Deque> doneQueue = new ArrayDeque>(); - - for (Match 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(source, target)); - } + List> renameLog = new ArrayList>(); try { - int renameCount = todoQueue.size(); - - for (Match match : todoQueue) { + for (Entry mapping : model.getRenameMap().entrySet()) { // rename file - if (!match.getValue().renameTo(match.getCandidate())) - throw new IOException(String.format("Failed to rename file: %s.", match.getValue().getName())); + if (!mapping.getKey().renameTo(mapping.getValue())) + throw new IOException(String.format("Failed to rename file: \"%s\".", mapping.getKey().getName())); - // revert in reverse order if renaming of all matches fails - doneQueue.addFirst(match); + // remember successfully renamed matches for possible revert + renameLog.add(mapping); } // renamed all matches successfully - Logger.getLogger("ui").info(String.format("%d files renamed.", renameCount)); - } catch (IOException e) { - // rename failed - Logger.getLogger("ui").warning(ExceptionUtilities.getRootCauseMessage(e)); + Logger.getLogger("ui").info(String.format("%d files renamed.", renameLog.size())); + } catch (Exception e) { + // could not rename one of the files, revert all changes + Logger.getLogger("ui").warning(e.getMessage()); - boolean revertSuccess = true; + // revert in reverse order + Collections.reverse(renameLog); // revert rename operations - for (Match match : doneQueue) { - revertSuccess &= match.getCandidate().renameTo(match.getValue()); - } - - if (!revertSuccess) { - Logger.getLogger("ui").severe("Failed to revert all rename operations."); + for (Entry mapping : renameLog) { + if (!mapping.getValue().renameTo(mapping.getKey())) { + Logger.getLogger("ui").severe(String.format("Failed to revert file: \"%s\".", mapping.getValue().getName())); + } } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java index dc916438..858381e5 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java @@ -2,6 +2,8 @@ package net.sourceforge.filebot.ui.panel.rename; +import static java.util.Collections.swap; + import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; @@ -58,16 +60,6 @@ class RenameList extends FileBotList { 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 AbstractAction upAction = new AbstractAction(null, ResourceManager.getIcon("action.up")) { @@ -76,7 +68,7 @@ class RenameList extends FileBotList { int index = getListComponent().getSelectedIndex(); if (index > 0) { - swap(index, index - 1); + swap(model, index, index - 1); getListComponent().setSelectedIndex(index - 1); } } @@ -88,7 +80,7 @@ class RenameList extends FileBotList { int index = getListComponent().getSelectedIndex(); if (index < model.size() - 1) { - swap(index, index + 1); + swap(model, index, index + 1); getListComponent().setSelectedIndex(index + 1); } } @@ -109,8 +101,8 @@ class RenameList extends FileBotList { public void mouseDragged(MouseEvent m) { int currentIndex = getListComponent().getSelectedIndex(); - if (currentIndex != lastIndex) { - swap(lastIndex, currentIndex); + if (currentIndex != lastIndex && lastIndex >= 0 && currentIndex >= 0) { + swap(model, lastIndex, currentIndex); lastIndex = currentIndex; } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java index 08e02948..4c6a82fb 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java @@ -12,7 +12,7 @@ import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.io.File; -import javax.swing.JLabel; +import javax.swing.DefaultListCellRenderer; import javax.swing.JList; import javax.swing.border.CompoundBorder; 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.tuned.FileUtilities; import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer; +import net.sourceforge.tuned.ui.GradientStyle; class RenameListCellRenderer extends DefaultFancyListCellRenderer { 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 noMatchGradientEndColor = new Color(0x9A9A9A); @@ -39,8 +40,8 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer { setHighlightingEnabled(false); - setLayout(new MigLayout("fill, insets 0", "align left", "align center")); - add(typeLabel, "gap rel:push"); + setLayout(new MigLayout("insets 0, fill", "align left", "align center")); + 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) { super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - // reset + // reset decoration setIcon(null); - typeLabel.setText(null); - typeLabel.setAlpha(1.0f); + typeRenderer.setVisible(false); + typeRenderer.setAlpha(1.0f); if (value instanceof File) { // display file extension File file = (File) value; - setText(FileUtilities.getName(file)); - typeLabel.setText(getType(file)); + if (renameModel.preserveExtension()) { + setText(FileUtilities.getName(file)); + typeRenderer.setText(getType(file)); + typeRenderer.setVisible(true); + } else { + setText(file.getName()); + } } else if (value instanceof FormattedFuture) { - // progress icon and value type + // display progress icon FormattedFuture future = (FormattedFuture) value; switch (future.getState()) { @@ -78,7 +84,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer { setGradientColors(noMatchGradientBeginColor, noMatchGradientEndColor); } else { 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 padding = new Insets(0, 6, 0, 5); @@ -110,7 +116,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer { private float alpha = 1.0f; - public TypeLabel() { + public TypeRenderer() { setOpaque(false); setForeground(new Color(0x141414)); @@ -128,7 +134,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer { 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.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) { this.alpha = alpha; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java index 68000310..9ce01043 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java @@ -7,6 +7,7 @@ import java.beans.PropertyChangeListener; import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; @@ -20,6 +21,7 @@ import javax.swing.SwingWorker; import javax.swing.SwingWorker.StateValue; import net.sourceforge.filebot.similarity.Match; +import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.ui.TunedUtilities; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.TransformedList; @@ -52,19 +54,9 @@ public class RenameModel extends MatchModel { } }; + 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 names() { return names; } @@ -75,16 +67,62 @@ public class RenameModel extends MatchModel { } - public List> getMatchesForRenaming() { - List> matches = new ArrayList>(); + public boolean preserveExtension() { + return preserveExtension; + } + + + public void setPreserveExtension(boolean preserveExtension) { + this.preserveExtension = preserveExtension; + } + + + public Map getRenameMap() { + Map map = new LinkedHashMap(); - for (int i = 0; i < size(); i++) { - if (hasComplement(i) && names.get(i).isDone()) { - matches.add(new Match(names().get(i).toString(), files().get(i))); + 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); + } + } + + // 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 matches; + return map; + } + + + public void useFormatter(Class type, MatchFormatter formatter) { + if (formatter != null) { + formatters.put(type, formatter); + } else { + formatters.remove(type); + } + + // reformat matches + names.refresh(); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java index e1544976..28b5f1f6 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java @@ -23,11 +23,9 @@ import java.util.prefs.Preferences; import javax.script.ScriptException; import javax.swing.AbstractAction; import javax.swing.Action; -import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; -import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; @@ -53,8 +51,8 @@ import net.sourceforge.tuned.PreferencesMap.AbstractAdapter; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; import net.sourceforge.tuned.ui.ActionPopup; import net.sourceforge.tuned.ui.LoadingOverlayPane; -import ca.odell.glazedlists.event.ListEvent; -import ca.odell.glazedlists.event.ListEventListener; +import ca.odell.glazedlists.ListSelection; +import ca.odell.glazedlists.swing.EventSelectionModel; public class RenamePanel extends JComponent { @@ -88,8 +86,8 @@ public class RenamePanel extends JComponent { namesList.getListComponent().setCellRenderer(cellrenderer); filesList.getListComponent().setCellRenderer(cellrenderer); - ListSelectionModel selectionModel = new DefaultListSelectionModel(); - selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + EventSelectionModel> selectionModel = new EventSelectionModel>(renameModel.matches()); + selectionModel.setSelectionMode(ListSelection.MULTIPLE_INTERVAL_SELECTION_DEFENSIVE); // use the same selection model for both lists to synchronize selection namesList.getListComponent().setSelectionModel(selectionModel); @@ -127,10 +125,6 @@ public class RenamePanel extends JComponent { add(renameButton, "gapy 30px, sizegroupx button"); add(new LoadingOverlayPane(namesList, namesList, "28px", "30px"), "grow, sizegroupx list"); - - // repaint on change - renameModel.names().addListEventListener(new RepaintHandler()); - renameModel.files().addListEventListener(new RepaintHandler()); } @@ -240,7 +234,7 @@ public class RenamePanel extends JComponent { // add remaining file entries renameModel.files().addAll(remainingFiles()); } catch (Exception e) { - Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e); + Logger.getLogger("ui").warning(ExceptionUtilities.getRootCauseMessage(e)); } finally { // auto-match finished namesList.firePropertyChange(LOADING_PROPERTY, true, false); @@ -278,7 +272,7 @@ public class RenamePanel extends JComponent { // multiple results have been found, user must select one SelectDialog selectDialog = new SelectDialog(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); @@ -299,17 +293,6 @@ public class RenamePanel extends JComponent { } } - - protected class RepaintHandler implements ListEventListener { - - @Override - public void listChanged(ListEvent listChanges) { - namesList.repaint(); - filesList.repaint(); - } - - }; - protected final PreferencesEntry persistentFormatExpression = Settings.userRoot().entry("rename.format", new AbstractAdapter() { @Override diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumCell.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumCell.java index 69194fab..7cf52644 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumCell.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumCell.java @@ -12,6 +12,7 @@ import java.util.concurrent.CancellationException; import javax.swing.SwingWorker.StateValue; import javax.swing.event.SwingPropertyChangeSupport; +import net.sourceforge.filebot.hash.HashType; import net.sourceforge.tuned.ExceptionUtilities; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java index 60775af2..c4650a03 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumComputationTask.java @@ -12,6 +12,9 @@ import java.util.concurrent.CancellationException; import javax.swing.SwingWorker; +import net.sourceforge.filebot.hash.Hash; +import net.sourceforge.filebot.hash.HashType; + class ChecksumComputationTask extends SwingWorker, Void> { diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java index 2d5f3997..d70865e6 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumRow.java @@ -16,6 +16,7 @@ import java.util.Set; import javax.swing.event.SwingPropertyChangeSupport; import net.sourceforge.filebot.FileBotUtilities; +import net.sourceforge.filebot.hash.HashType; class ChecksumRow { diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java index b01a9d66..e56dd423 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java @@ -9,6 +9,8 @@ import java.util.Date; import java.util.Formatter; 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.tuned.FileUtilities; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java index 654d9cac..9e53fc01 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableModel.java @@ -18,6 +18,7 @@ import java.util.Set; import javax.swing.table.AbstractTableModel; +import net.sourceforge.filebot.hash.HashType; import net.sourceforge.tuned.FileUtilities; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableTransferablePolicy.java index e5b290fe..b2bafd12 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableTransferablePolicy.java @@ -15,7 +15,9 @@ import java.util.concurrent.ExecutorService; import java.util.logging.Level; import java.util.logging.Logger; -import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScanner.IllegalSyntaxException; +import net.sourceforge.filebot.hash.HashType; +import net.sourceforge.filebot.hash.IllegalSyntaxException; +import net.sourceforge.filebot.hash.VerificationFileScanner; import net.sourceforge.filebot.ui.transfer.BackgroundFileTransferablePolicy; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter; diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java b/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java index 0b478b1d..c86632d3 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java @@ -30,6 +30,7 @@ import javax.swing.border.TitledBorder; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.hash.HashType; import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.ui.transfer.DefaultTransferHandler; import net.sourceforge.filebot.ui.transfer.LoadAction; diff --git a/source/net/sourceforge/tuned/DefaultThreadFactory.java b/source/net/sourceforge/tuned/DefaultThreadFactory.java index 6e29468c..0a6ab8ef 100644 --- a/source/net/sourceforge/tuned/DefaultThreadFactory.java +++ b/source/net/sourceforge/tuned/DefaultThreadFactory.java @@ -26,7 +26,10 @@ public class DefaultThreadFactory implements ThreadFactory { 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.priority = priority; @@ -45,4 +48,9 @@ public class DefaultThreadFactory implements ThreadFactory { return thread; } + + public ThreadGroup getThreadGroup() { + return group; + } + } diff --git a/source/net/sourceforge/tuned/ExceptionUtilities.java b/source/net/sourceforge/tuned/ExceptionUtilities.java index f5afa28e..c908d327 100644 --- a/source/net/sourceforge/tuned/ExceptionUtilities.java +++ b/source/net/sourceforge/tuned/ExceptionUtilities.java @@ -13,6 +13,19 @@ public final class ExceptionUtilities { } + @SuppressWarnings("unchecked") + public static T findCause(Throwable t, Class type) { + while (t != null) { + if (type.isInstance(t)) + return (T) t; + + t = t.getCause(); + } + + return null; + } + + public static String getRootCauseMessage(Throwable t) { return getMessage(getRootCause(t)); } @@ -22,7 +35,7 @@ public final class ExceptionUtilities { String message = t.getMessage(); if (message == null || message.isEmpty()) { - message = t.toString().replaceAll(t.getClass().getName(), t.getClass().getSimpleName()); + message = t.toString(); } return message; diff --git a/source/net/sourceforge/tuned/ui/DefaultFancyListCellRenderer.java b/source/net/sourceforge/tuned/ui/DefaultFancyListCellRenderer.java index 713d95bc..bd98d376 100644 --- a/source/net/sourceforge/tuned/ui/DefaultFancyListCellRenderer.java +++ b/source/net/sourceforge/tuned/ui/DefaultFancyListCellRenderer.java @@ -5,6 +5,7 @@ package net.sourceforge.tuned.ui; import java.awt.Color; import java.awt.Insets; +import javax.swing.DefaultListCellRenderer; import javax.swing.Icon; import javax.swing.JLabel; import javax.swing.JList; @@ -12,7 +13,7 @@ import javax.swing.JList; public class DefaultFancyListCellRenderer extends AbstractFancyListCellRenderer { - private final JLabel label = new JLabel(); + private final JLabel label = new DefaultListCellRenderer(); public DefaultFancyListCellRenderer() { diff --git a/test/net/sourceforge/filebot/FileBotTestSuite.java b/test/net/sourceforge/filebot/FileBotTestSuite.java index eae1d849..64681d4d 100644 --- a/test/net/sourceforge/filebot/FileBotTestSuite.java +++ b/test/net/sourceforge/filebot/FileBotTestSuite.java @@ -3,9 +3,9 @@ package net.sourceforge.filebot; import net.sourceforge.filebot.format.ExpressionFormatTest; +import net.sourceforge.filebot.hash.VerificationFileScannerTest; import net.sourceforge.filebot.similarity.SimilarityTestSuite; import net.sourceforge.filebot.ui.panel.rename.MatchModelTest; -import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScannerTest; import net.sourceforge.filebot.web.WebTestSuite; import org.junit.runner.RunWith; diff --git a/test/net/sourceforge/filebot/ui/panel/sfv/VerificationFileScannerTest.java b/test/net/sourceforge/filebot/hash/VerificationFileScannerTest.java similarity index 91% rename from test/net/sourceforge/filebot/ui/panel/sfv/VerificationFileScannerTest.java rename to test/net/sourceforge/filebot/hash/VerificationFileScannerTest.java index 010a9843..57b5397b 100644 --- a/test/net/sourceforge/filebot/ui/panel/sfv/VerificationFileScannerTest.java +++ b/test/net/sourceforge/filebot/hash/VerificationFileScannerTest.java @@ -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.util.Scanner;