diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index 64b8ed80..a5cb3aea 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -144,6 +144,8 @@ public class Main { initializeSecurityManager(); // update system properties + System.setProperty("grape.root", new File(getApplicationFolder(), "grape").getAbsolutePath()); + if (System.getProperty("http.agent") == null) { System.setProperty("http.agent", String.format("%s %s", getApplicationName(), getApplicationVersion())); } diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index b4c247f1..e278e007 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -99,8 +99,6 @@ public class ArgumentProcessor { } } else { // execute user script - System.setProperty("grape.root", new File(getApplicationFolder(), "grape").getAbsolutePath()); - Bindings bindings = new SimpleBindings(); bindings.put("args", args.getFiles(false)); diff --git a/source/net/sourceforge/filebot/cli/GroovyPad.java b/source/net/sourceforge/filebot/cli/GroovyPad.java new file mode 100644 index 00000000..6dd7cee8 --- /dev/null +++ b/source/net/sourceforge/filebot/cli/GroovyPad.java @@ -0,0 +1,328 @@ +package net.sourceforge.filebot.cli; + +import static net.sourceforge.tuned.ui.TunedUtilities.*; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dialog.ModalExclusionType; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.AccessController; +import java.util.concurrent.TimeUnit; + +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JSplitPane; +import javax.swing.JToolBar; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +import net.sourceforge.filebot.Analytics; +import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.Settings; +import net.sourceforge.filebot.cli.ArgumentProcessor.DefaultScriptProvider; +import net.sourceforge.tuned.TeePrintStream; + +import org.fife.ui.rsyntaxtextarea.FileLocation; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.TextEditorPane; +import org.fife.ui.rtextarea.RTextScrollPane; + +public class GroovyPad extends JFrame { + + public GroovyPad() throws IOException { + super("Groovy Pad"); + + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, createEditor(), createOutputLog()); + splitPane.setResizeWeight(0.7); + + JComponent c = (JComponent) getContentPane(); + c.setLayout(new BorderLayout(0, 0)); + c.add(splitPane, BorderLayout.CENTER); + + JToolBar tools = new JToolBar("Run", JToolBar.HORIZONTAL); + tools.setFloatable(true); + tools.add(action_run); + tools.add(action_cancel); + c.add(tools, BorderLayout.NORTH); + + action_run.setEnabled(true); + action_cancel.setEnabled(false); + + installAction(c, KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), action_run); + installAction(c, KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK), action_run); + + addWindowListener(new WindowAdapter() { + + @Override + public void windowClosed(WindowEvent evt) { + action_cancel.actionPerformed(null); + console.unhook(); + + try { + editor.save(); + } catch (IOException e) { + // ignore + } + } + }); + + console = new MessageConsole(output); + console.hook(); + + shell = createScriptShell(); + + setModalExclusionType(ModalExclusionType.TOOLKIT_EXCLUDE); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLocationByPlatform(true); + setSize(800, 600); + + } + + protected MessageConsole console; + protected TextEditorPane editor; + protected TextEditorPane output; + + protected JComponent createEditor() throws IOException { + editor = new TextEditorPane(TextEditorPane.INSERT_MODE, false, getFileLocation("pad.groovy"), "UTF-8"); + editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_GROOVY); + editor.setAutoscrolls(false); + editor.setAnimateBracketMatching(false); + editor.setAntiAliasingEnabled(true); + editor.setAutoIndentEnabled(true); + editor.setBracketMatchingEnabled(true); + editor.setCloseCurlyBraces(true); + editor.setClearWhitespaceLinesEnabled(true); + editor.setCodeFoldingEnabled(true); + editor.setHighlightSecondaryLanguages(false); + editor.setRoundedSelectionEdges(false); + editor.setTabsEmulated(false); + + // restore on open + editor.reload(); + + return new RTextScrollPane(editor, true); + } + + protected JComponent createOutputLog() throws IOException { + output = new TextEditorPane(TextEditorPane.OVERWRITE_MODE, false); + output.setEditable(false); + output.setReadOnly(true); + output.setAutoscrolls(true); + output.setBackground(new Color(255, 255, 218)); + + output.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE); + output.setAnimateBracketMatching(false); + output.setAntiAliasingEnabled(true); + output.setAutoIndentEnabled(false); + output.setBracketMatchingEnabled(false); + output.setCloseCurlyBraces(false); + output.setClearWhitespaceLinesEnabled(false); + output.setCodeFoldingEnabled(false); + output.setHighlightCurrentLine(false); + output.setHighlightSecondaryLanguages(false); + output.setRoundedSelectionEdges(false); + output.setTabsEmulated(false); + + return new RTextScrollPane(output, true); + } + + protected FileLocation getFileLocation(String name) throws IOException { + File pad = new File(Settings.getApplicationFolder(), name); + if (!pad.exists()) { + pad.createNewFile(); + } + return FileLocation.create(pad); + } + + protected final ScriptShell shell; + + protected ScriptShell createScriptShell() { + try { + DefaultScriptProvider scriptProvider = new DefaultScriptProvider(true); + scriptProvider.setBaseScheme(new URI("fn", "%s", null)); + + return new ScriptShell(new CmdlineOperations(), new ArgumentBean(), AccessController.getContext(), scriptProvider); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected final Action action_run = new AbstractAction("Run", ResourceManager.getIcon("script.go")) { + + @Override + public void actionPerformed(ActionEvent evt) { + // persist script file and clear output + try { + editor.save(); + } catch (IOException e) { + // won't happen + } + output.setText(""); + + if (currentRunner == null || currentRunner.isDone()) { + currentRunner = new Runner(editor.getText()) { + + @Override + protected void done() { + action_run.setEnabled(true); + action_cancel.setEnabled(false); + } + }; + + action_run.setEnabled(false); + action_cancel.setEnabled(true); + currentRunner.execute(); + } + } + }; + + protected final Action action_cancel = new AbstractAction("Cancel", ResourceManager.getIcon("script.cancel")) { + + @Override + public void actionPerformed(ActionEvent evt) { + if (currentRunner != null && !currentRunner.isDone()) { + currentRunner.cancel(true); + currentRunner.getExecutionThread().stop(); + + try { + currentRunner.get(2, TimeUnit.SECONDS); + } catch (Exception e) { + // ignore + } + } + } + }; + + private Runner currentRunner = null; + + protected class Runner extends SwingWorker { + + private Thread executionThread; + private Object result; + + public Runner(final String script) { + executionThread = new Thread("GroovyPadRunner") { + + @Override + public void run() { + try { + result = shell.evaluate(script, new SimpleBindings(), true); + + // make sure to flush Groovy output + shell.evaluate("println()", new SimpleBindings(), true); + } catch (ScriptException e) { + e.getCause().getCause().printStackTrace(); + } catch (Throwable e) { + e.printStackTrace(); + } + }; + }; + + executionThread.setDaemon(false); + executionThread.setPriority(Thread.MIN_PRIORITY); + } + + @Override + protected Object doInBackground() throws Exception { + executionThread.start(); + executionThread.join(); + return result; + } + + public Thread getExecutionThread() { + return executionThread; + } + }; + + public static class MessageConsole { + + private final PrintStream system_out = System.out; + private final PrintStream system_err = System.err; + + private JTextComponent textComponent; + + public MessageConsole(JTextComponent textComponent) { + this.textComponent = textComponent; + } + + public void hook() { + try { + System.setOut(new TeePrintStream(new ConsoleOutputStream(), true, "UTF-8", system_out)); + System.setErr(new TeePrintStream(new ConsoleOutputStream(), true, "UTF-8", system_err)); + } catch (UnsupportedEncodingException e) { + // can't happen + } + } + + public void unhook() { + System.setOut(system_out); + System.setErr(system_err); + } + + private class ConsoleOutputStream extends ByteArrayOutputStream { + + public void flush() { + try { + String message = this.toString("UTF-8"); + reset(); + + commit(message); + } catch (UnsupportedEncodingException e) { + // can't happen + } + } + + private void commit(final String line) { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + try { + int offset = textComponent.getDocument().getLength(); + textComponent.getDocument().insertString(offset, line, null); + textComponent.setCaretPosition(textComponent.getDocument().getLength()); + } catch (BadLocationException e) { + // ignore + } + } + }); + } + } + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + try { + // ignore analytics in developer mode + Analytics.setEnabled(false); + + GroovyPad pad = new GroovyPad(); + pad.setDefaultCloseOperation(EXIT_ON_CLOSE); + pad.setVisible(true); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + +} diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.java b/source/net/sourceforge/filebot/cli/ScriptShell.java index 77aa83a6..2efe79ba 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.java +++ b/source/net/sourceforge/filebot/cli/ScriptShell.java @@ -1,7 +1,5 @@ - package net.sourceforge.filebot.cli; - import static net.sourceforge.filebot.cli.CLILogging.*; import java.awt.AWTPermission; @@ -41,70 +39,61 @@ import net.sourceforge.filebot.web.MovieIdentificationService; import org.codehaus.groovy.jsr223.GroovyScriptEngineFactory; import org.codehaus.groovy.runtime.StackTraceUtils; +public class ScriptShell { -class ScriptShell { - private final ScriptEngine engine = new GroovyScriptEngineFactory().getScriptEngine(); - + private final ScriptProvider scriptProvider; - - + public ScriptShell(CmdlineInterface cli, ArgumentBean args, AccessControlContext acc, ScriptProvider scriptProvider) throws ScriptException { this.scriptProvider = scriptProvider; - + // setup script context ScriptContext context = new SimpleScriptContext(); context.setBindings(initializeBindings(cli, args, acc), ScriptContext.GLOBAL_SCOPE); engine.setContext(context); - + // import additional functions into the shell environment engine.eval(new InputStreamReader(ExpressionFormat.class.getResourceAsStream("ExpressionFormat.lib.groovy"))); engine.eval(new InputStreamReader(ScriptShell.class.getResourceAsStream("ScriptShell.lib.groovy"))); } - - + public static interface ScriptProvider { - + public URI getScriptLocation(String input); - - + public Script fetchScript(URI uri) throws Exception; } - - + public static class Script { - + public final String code; public final boolean trusted; - - + public Script(String code, boolean trusted) { this.code = code; this.trusted = trusted; } } - - + public Object runScript(String input, Bindings bindings) throws Throwable { return runScript(scriptProvider.getScriptLocation(input), bindings); } - - + public Object runScript(URI resource, Bindings bindings) throws Throwable { Script script = scriptProvider.fetchScript(resource); return evaluate(script.code, bindings, script.trusted); } - - + public Object evaluate(final String script, final Bindings bindings, boolean trustScript) throws Throwable { try { if (trustScript) { return engine.eval(script, bindings); } - + try { return AccessController.doPrivileged(new PrivilegedExceptionAction() { - + @Override public Object run() throws ScriptException { return engine.eval(script, bindings); @@ -117,24 +106,23 @@ class ScriptShell { throw StackTraceUtils.deepSanitize(e); // make Groovy stack human-readable } } - - + protected Bindings initializeBindings(CmdlineInterface cli, ArgumentBean args, AccessControlContext acc) { Bindings bindings = new SimpleBindings(); - + // bind external parameters if (args.bindings != null) { for (Entry it : args.bindings) { bindings.put(it.getKey(), it.getValue()); } } - + // bind API objects bindings.put("_cli", PrivilegedInvocation.newProxy(CmdlineInterface.class, cli, acc)); - bindings.put("_script", new File(args.script)); + bindings.put("_script", args.script); bindings.put("_args", args); bindings.put("_shell", this); - + Map defines = new LinkedHashMap(); if (args.bindings != null) { for (Entry it : args.bindings) { @@ -142,34 +130,33 @@ class ScriptShell { } } bindings.put("_def", defines); - + bindings.put("_types", MediaTypes.getDefault()); bindings.put("_log", CLILogger); - + // bind Java properties and environment variables bindings.put("_system", new AssociativeScriptObject(System.getProperties())); bindings.put("_environment", new AssociativeScriptObject(System.getenv())); - + // bind console object bindings.put("console", System.console()); - + // bind Episode data providers for (EpisodeListProvider service : WebServices.getEpisodeListProviders()) { bindings.put(service.getName(), service); } - + // bind Movie data providers for (MovieIdentificationService service : WebServices.getMovieIdentificationServices()) { bindings.put(service.getName(), service); } - + return bindings; } - - + protected AccessControlContext getSandboxAccessControlContext() { Permissions permissions = new Permissions(); - + permissions.add(new RuntimePermission("createClassLoader")); permissions.add(new RuntimePermission("accessClassInPackage.*")); permissions.add(new RuntimePermission("modifyThread")); @@ -179,22 +166,22 @@ class ScriptShell { permissions.add(new RuntimePermission("getenv.*")); permissions.add(new RuntimePermission("getFileSystemAttributes")); permissions.add(new ManagementPermission("monitor")); - + // write permissions for temp and cache folders permissions.add(new FilePermission(new File(System.getProperty("ehcache.disk.store.dir")).getAbsolutePath() + File.separator + "-", "write, delete")); permissions.add(new FilePermission(new File(System.getProperty("java.io.tmpdir")).getAbsolutePath() + File.separator + "-", "write, delete")); - + // AWT / Swing permissions permissions.add(new AWTPermission("accessEventQueue")); permissions.add(new AWTPermission("toolkitModality")); permissions.add(new AWTPermission("showWindowWithoutWarningBanner")); - + // this is probably a security problem but nevermind permissions.add(new RuntimePermission("accessDeclaredMembers")); permissions.add(new ReflectPermission("suppressAccessChecks")); permissions.add(new RuntimePermission("modifyThread")); - + return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, permissions) }); } - + } diff --git a/source/net/sourceforge/filebot/resources/script.cancel.png b/source/net/sourceforge/filebot/resources/script.cancel.png new file mode 100644 index 00000000..1514d51a Binary files /dev/null and b/source/net/sourceforge/filebot/resources/script.cancel.png differ diff --git a/source/net/sourceforge/filebot/resources/script.go.png b/source/net/sourceforge/filebot/resources/script.go.png new file mode 100644 index 00000000..8e154e23 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/script.go.png differ diff --git a/source/net/sourceforge/filebot/ui/MainFrame.java b/source/net/sourceforge/filebot/ui/MainFrame.java index 394dea1f..dfa6b5dc 100644 --- a/source/net/sourceforge/filebot/ui/MainFrame.java +++ b/source/net/sourceforge/filebot/ui/MainFrame.java @@ -37,6 +37,7 @@ import net.sf.ehcache.CacheManager; import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.Settings; +import net.sourceforge.filebot.cli.GroovyPad; import net.sourceforge.filebot.ui.analyze.AnalyzePanelBuilder; import net.sourceforge.filebot.ui.episodelist.EpisodeListPanelBuilder; import net.sourceforge.filebot.ui.list.ListPanelBuilder; @@ -111,6 +112,15 @@ public class MainFrame extends JFrame { UILogger.info("Cache has been cleared"); } }); + + TunedUtilities.installAction(this.getRootPane(), getKeyStroke(VK_F5, 0), new AbstractAction("Run") { + + @Override + public void actionPerformed(ActionEvent evt) { + MainFrame.this.dispose(); + GroovyPad.main(new String[0]); + } + }); } public static PanelBuilder[] createPanelBuilders() {