+ 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
created programmtically using CacheManager.add(String cacheName).
created pragmatically using CacheManager.add(String cacheName).
-->
<defaultCache
maxElementsInMemory="100"

View File

@ -23,9 +23,6 @@ public class ArgumentBean {
@Option(name = "-clear", usage = "Clear history and settings")
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>")
private boolean sfv;
@ -48,11 +45,6 @@ public class ArgumentBean {
}
public boolean analyze() {
return analyze;
}
public List<File> arguments() {
return arguments;
}

View File

@ -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:");

View File

@ -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<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 {
// try to get checksum from cache
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 {
protected final Object bean;
protected final Object bindingBean;
protected final Map<String, Method> bindings = new HashMap<String, Method>();
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<String, Object> 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);
}

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;
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("<<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
public Object parseObject(String source, ParsePosition pos) {
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;
class ChecksumHash implements Hash {
public class ChecksumHash implements Hash {
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);

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.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.
*
* <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));
}
};
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);
}
},

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;
@ -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;

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;
@ -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);
}

View File

@ -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<Entry<File, String>>, Closeable {
public class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeable {
private final Scanner scanner;
@ -48,7 +48,7 @@ class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeabl
@Override
public Entry<File, String> next() {
public Entry<File, String> next() throws IllegalSyntaxException {
// cache next line
if (!hasNext()) {
throw new NoSuchElementException();
@ -88,10 +88,10 @@ class VerificationFileScanner implements Iterator<Entry<File, String>>, Closeabl
* | Group 1 | | Group 2 |
* </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);
if (!matcher.matches())
@ -127,18 +127,4 @@ class VerificationFileScanner implements Iterator<Entry<File, String>>, 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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<String> examples = new ArrayList<String>();
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<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
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() {

View File

@ -3,11 +3,11 @@ syntax: <html><b>{</b> <b>}</b> ... expression, <b>n</b> ... name, <b>s</b> ...
# 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)}

View File

@ -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");
}

View File

@ -15,7 +15,7 @@ import ca.odell.glazedlists.TransformedList;
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>>();

View File

@ -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<Object> model;
private final List<Object> model;
public NamesListTransferablePolicy(EventList<Object> model) {
public NamesListTransferablePolicy(List<Object> model) {
this.model = model;
}

View File

@ -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<Match<File, File>> todoQueue = new ArrayDeque<Match<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));
}
List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
try {
int renameCount = todoQueue.size();
for (Match<File, File> match : todoQueue) {
for (Entry<File, File> 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<File, File> match : doneQueue) {
revertSuccess &= match.getCandidate().renameTo(match.getValue());
}
if (!revertSuccess) {
Logger.getLogger("ui").severe("Failed to revert all rename operations.");
for (Entry<File, File> mapping : renameLog) {
if (!mapping.getValue().renameTo(mapping.getKey())) {
Logger.getLogger("ui").severe(String.format("Failed to revert file: \"%s\".", mapping.getValue().getName()));
}
}
}

View File

@ -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<E> extends FileBotList<E> {
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<E> extends FileBotList<E> {
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<E> extends FileBotList<E> {
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<E> extends FileBotList<E> {
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;
}
}

View File

@ -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;
}

View File

@ -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<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() {
return names;
}
@ -75,16 +67,62 @@ public class RenameModel extends MatchModel<Object, File> {
}
public List<Match<String, File>> getMatchesForRenaming() {
List<Match<String, File>> matches = new ArrayList<Match<String, File>>();
public boolean preserveExtension() {
return preserveExtension;
}
public void setPreserveExtension(boolean preserveExtension) {
this.preserveExtension = preserveExtension;
}
public Map<File, File> getRenameMap() {
Map<File, File> map = new LinkedHashMap<File, File>();
for (int i = 0; i < size(); i++) {
if (hasComplement(i) && names.get(i).isDone()) {
matches.add(new Match<String, File>(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();
}

View File

@ -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<Match<Object, File>> selectionModel = new EventSelectionModel<Match<Object, File>>(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<Object>());
renameModel.files().addListEventListener(new RepaintHandler<Object>());
}
@ -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<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);
@ -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>() {
@Override

View File

@ -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;

View File

@ -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<Map<HashType, String>, Void> {

View File

@ -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 {

View File

@ -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;

View File

@ -18,6 +18,7 @@ import java.util.Set;
import javax.swing.table.AbstractTableModel;
import net.sourceforge.filebot.hash.HashType;
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.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;

View File

@ -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;

View File

@ -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;
}
}

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) {
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;

View File

@ -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() {

View File

@ -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;

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.util.Scanner;