From 1a730c3ec61e1f571ca56e39f3fafbbd2291eacb Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Tue, 15 Apr 2014 12:23:58 +0000 Subject: [PATCH] * finish rewrite of ExpressionFormat customizations --- .../filebot/format/ExpressionFormat.java | 118 ++++---- .../format/ExpressionFormatFunctions.java | 55 ++++ .../format/ExpressionFormatMethods.java | 256 +++++++++++++++++- 3 files changed, 367 insertions(+), 62 deletions(-) create mode 100644 source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.java b/source/net/sourceforge/filebot/format/ExpressionFormat.java index f1ab12f5..82738e4e 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.java +++ b/source/net/sourceforge/filebot/format/ExpressionFormat.java @@ -1,13 +1,11 @@ - package net.sourceforge.filebot.format; - import static net.sourceforge.filebot.util.ExceptionUtilities.*; import static net.sourceforge.filebot.util.FileUtilities.*; +import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyRuntimeException; import groovy.lang.MissingPropertyException; -import java.io.InputStreamReader; import java.security.AccessController; import java.text.FieldPosition; import java.text.Format; @@ -25,25 +23,35 @@ import javax.script.ScriptEngine; import javax.script.ScriptException; import javax.script.SimpleScriptContext; +import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.MultipleCompilationErrorsException; -import org.codehaus.groovy.jsr223.GroovyScriptEngineFactory; - +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl; public class ExpressionFormat extends Format { - + private static ScriptEngine engine; private static Map scriptletCache = new HashMap(); - - + + protected static ScriptEngine createScriptEngine() { + CompilerConfiguration config = new CompilerConfiguration(); + + // include default functions + ImportCustomizer imports = new ImportCustomizer(); + imports.addStaticStars(ExpressionFormatFunctions.class.getName()); + config.addCompilationCustomizers(imports); + + GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config); + return new GroovyScriptEngineImpl(classLoader); + } + protected static synchronized ScriptEngine getGroovyScriptEngine() throws ScriptException { if (engine == null) { - engine = new GroovyScriptEngineFactory().getScriptEngine(); - engine.eval(new InputStreamReader(ExpressionFormat.class.getResourceAsStream("ExpressionFormat.lib.groovy"))); + engine = createScriptEngine(); } return engine; } - - + protected static synchronized CompiledScript compileScriptlet(String expression) throws ScriptException { Compilable engine = (Compilable) getGroovyScriptEngine(); CompiledScript scriptlet = scriptletCache.get(expression); @@ -53,38 +61,35 @@ public class ExpressionFormat extends Format { } return scriptlet; } - + private final String expression; - + private final Object[] compilation; - + private ScriptException lastException; - - + public ExpressionFormat(String expression) throws ScriptException { this.expression = expression; this.compilation = secure(compile(expression)); } - - + public String getExpression() { return expression; } - - + protected Object[] compile(String expression) throws ScriptException { List compilation = new ArrayList(); - + char open = '{'; char close = '}'; - + StringBuilder token = new StringBuilder(); int level = 0; - + // parse expressions and literals for (int i = 0; i < expression.length(); i++) { char c = expression.charAt(i); - + if (c == open) { if (level == 0) { if (token.length() > 0) { @@ -94,7 +99,7 @@ public class ExpressionFormat extends Format { } else { token.append(c); } - + level++; } else if (c == close) { if (level == 1) { @@ -104,14 +109,14 @@ public class ExpressionFormat extends Format { } catch (ScriptException e) { // try to extract syntax exception ScriptException illegalSyntax = e; - + try { String message = findCause(e, MultipleCompilationErrorsException.class).getErrorCollector().getSyntaxError(0).getOriginalMessage(); illegalSyntax = new ScriptException("SyntaxError: " + message); } catch (Exception ignore) { // ignore, just use original exception } - + throw illegalSyntax; } finally { token.setLength(0); @@ -120,65 +125,62 @@ public class ExpressionFormat extends Format { } else { token.append(c); } - + level--; } else { token.append(c); } - + // sanity check if (level < 0) { throw new ScriptException("SyntaxError: unexpected token: " + close); } } - + // sanity check if (level != 0) { throw new ScriptException("SyntaxError: missing token: " + close); } - + // append tail if (token.length() > 0) { compilation.add(token.toString()); } - + return compilation.toArray(); } - - + public Bindings getBindings(Object value) { return new ExpressionBindings(value) { - + @Override public Object get(Object key) { return normalizeBindingValue(super.get(key)); } }; } - - + @Override public StringBuffer format(Object object, StringBuffer sb, FieldPosition pos) { return format(getBindings(object), sb); } - - + public StringBuffer format(Bindings bindings, StringBuffer sb) { // use privileged bindings so we are not restricted by the script sandbox Bindings priviledgedBindings = PrivilegedInvocation.newProxy(Bindings.class, bindings, AccessController.getContext()); - + // initialize script context with the privileged bindings ScriptContext context = new SimpleScriptContext(); context.setBindings(priviledgedBindings, ScriptContext.GLOBAL_SCOPE); - + // reset exception state lastException = null; - + for (Object snipped : compilation) { if (snipped instanceof CompiledScript) { try { Object value = normalizeExpressionValue(((CompiledScript) snipped).eval(context)); - + if (value != null) { sb.append(value); } @@ -189,27 +191,24 @@ public class ExpressionFormat extends Format { sb.append(snipped); } } - + return sb; } - - + protected Object normalizeBindingValue(Object value) { // if the binding value is a String, remove illegal characters if (value instanceof CharSequence) { return replacePathSeparators(value.toString()).trim(); } - + // if the binding value is an Object, just leave it return value; } - - + protected Object normalizeExpressionValue(Object value) { return value; } - - + protected void handleException(ScriptException exception) { if (findCause(exception, MissingPropertyException.class) != null) { lastException = new ExpressionException(new BindingException(findCause(exception, MissingPropertyException.class).getProperty(), "undefined", exception)); @@ -219,29 +218,26 @@ public class ExpressionFormat extends Format { lastException = exception; } } - - + public ScriptException caughtScriptException() { return lastException; } - - + private Object[] secure(Object[] compilation) { for (int i = 0; i < compilation.length; i++) { Object snipped = compilation[i]; - + if (snipped instanceof CompiledScript) { compilation[i] = new SecureCompiledScript((CompiledScript) snipped); } } - + return compilation; } - - + @Override public Object parseObject(String source, ParsePosition pos) { throw new UnsupportedOperationException(); } - + } diff --git a/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java b/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java new file mode 100644 index 00000000..70657d5b --- /dev/null +++ b/source/net/sourceforge/filebot/format/ExpressionFormatFunctions.java @@ -0,0 +1,55 @@ +package net.sourceforge.filebot.format; + +import groovy.lang.Closure; + +import java.util.ArrayList; +import java.util.List; + +/** + * Global functions available in the {@link ExpressionFormat} + */ +public class ExpressionFormatFunctions { + + /** + * General helpers and utilities + */ + public static Object c(Closure c) { + try { + return c.call(); + } catch (Exception e) { + return null; + } + } + + public static Object any(Closure... closures) { + for (Closure it : closures) { + try { + Object result = it.call(); + if (result != null) { + return result; + } + } catch (Exception e) { + // ignore + } + } + return null; + } + + public static List allOf(Closure... closures) { + List values = new ArrayList(); + + for (Closure it : closures) { + try { + Object result = it.call(); + if (result != null) { + values.add(result); + } + } catch (Exception e) { + // ignore + } + } + + return values; + } + +} diff --git a/source/net/sourceforge/filebot/format/ExpressionFormatMethods.java b/source/net/sourceforge/filebot/format/ExpressionFormatMethods.java index d313af3f..ce4a335d 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormatMethods.java +++ b/source/net/sourceforge/filebot/format/ExpressionFormatMethods.java @@ -1,7 +1,21 @@ package net.sourceforge.filebot.format; +import static java.util.regex.Pattern.*; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; + +import net.sourceforge.filebot.util.FileUtilities; + +import com.ibm.icu.text.Transliterator; + public class ExpressionFormatMethods { + /** + * Convenience methods for String.toLowerCase() and String.toUpperCase() + */ public static String lower(String self) { return self.toLowerCase(); } @@ -10,6 +24,11 @@ public class ExpressionFormatMethods { return self.toUpperCase(); } + /** + * Pad strings or numbers with given characters ('0' by default). + * + * e.g. "1" -> "01" + */ public static String pad(String self, int length, String padding) { while (self.length() < length) { self = padding + self; @@ -22,6 +41,241 @@ public class ExpressionFormatMethods { } public static String pad(Number self, int length) { - return pad(self.toString(), length); + return pad(self.toString(), length, "0"); } + + /** + * Return a substring matching the given pattern or break. + */ + public static String match(String self, String pattern) { + return match(self, pattern, -1); + } + + public static String match(String self, String pattern, int matchGroup) { + Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CASE | MULTILINE).matcher(self); + if (matcher.find()) { + return (matcher.groupCount() > 0 && matchGroup < 0 ? matcher.group(1) : matcher.group(matchGroup < 0 ? 0 : matchGroup)).trim(); + } else { + throw new IllegalArgumentException("Pattern not found"); + } + } + + /** + * Return a list of all matching patterns or break. + */ + public static List matchAll(String self, String pattern) { + return matchAll(self, pattern, 0); + } + + public static List matchAll(String self, String pattern, int matchGroup) { + List matches = new ArrayList(); + Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CASE | MULTILINE).matcher(self); + while (matcher.find()) { + matches.add(matcher.group(matchGroup).trim()); + } + + if (matches.size() > 0) { + return matches; + } else { + throw new IllegalArgumentException("Pattern not found"); + } + } + + public static String removeAll(String self, String pattern) { + return compile(pattern, CASE_INSENSITIVE | UNICODE_CASE | MULTILINE).matcher(self).replaceAll("").trim(); + } + + /** + * Replace space characters with a given characters. + * + * e.g. "Doctor Who" -> "Doctor_Who" + */ + public static String space(String self, String replacement) { + return self.replaceAll("[:?._]", " ").trim().replaceAll("\\s+", replacement); + } + + /** + * Upper-case all initials. + * + * e.g. "The Day a new Demon was born" -> "The Day A New Demon Was Born" + */ + public static String upperInitial(String self) { + Matcher matcher = compile("(?<=[&()+.,-;<=>?\\[\\]_{|}~ ]|^)[a-z]").matcher(self); + + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(buffer, matcher.group().toUpperCase()); + } + matcher.appendTail(buffer); + + return buffer.toString(); + } + + public static String sortName(String self) { + return sortName(self, "$2, $1"); + } + + public static String sortName(String self, String replacement) { + return compile("^(The|A|An)\\s(.+)", CASE_INSENSITIVE).matcher(self).replaceFirst(replacement).trim(); + } + + /** + * Get acronym, i.e. first letter of each word. + * + * e.g. "Deep Space 9" -> "DS9" + */ + public static String acronym(String self) { + String name = sortName(self, "$2"); + Matcher matcher = compile("(?<=[&()+.,-;<=>?\\[\\]_{|}~ ]|^)[\\p{Alnum}]").matcher(name); + + StringBuilder buffer = new StringBuilder(); + while (matcher.find()) { + buffer.append(matcher.group().toUpperCase()); + } + + return buffer.toString(); + } + + /** + * Lower-case all letters that are not initials. + * + * e.g. "Gundam SEED" -> "Gundam Seed" + */ + public static String lowerTrail(String self) { + Matcher matcher = compile("\\b(\\p{Alpha})(\\p{Alpha}+)\\b").matcher(self); + + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(buffer, matcher.group(1) + matcher.group(2).toLowerCase()); + } + matcher.appendTail(buffer); + + return buffer.toString(); + } + + /** + * Return substring before the given pattern. + */ + public static String before(String self, String pattern) { + Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CASE).matcher(self); + + // pattern was found, return leading substring, else return original value + return matcher.find() ? self.substring(0, matcher.start()).trim() : self; + } + + /** + * Return substring after the given pattern. + */ + public static String after(String self, String pattern) { + Matcher matcher = compile(pattern, CASE_INSENSITIVE | UNICODE_CASE).matcher(self); + + // pattern was found, return trailing substring, else return original value + return matcher.find() ? self.substring(matcher.end(), self.length()).trim() : self; + } + + /** + * Replace trailing parenthesis including any leading whitespace. + * + * e.g. "The IT Crowd (UK)" -> "The IT Crowd" + */ + public static String replaceTrailingBrackets(String self) { + return replaceTrailingBrackets(self, ""); + } + + public static String replaceTrailingBrackets(String self, String replacement) { + return self.replaceAll("\\s*[(]([^)]*)[)]$", replacement).trim(); + } + + /** + * Replace 'part identifier'. + * + * e.g. "Today Is the Day: Part 1" -> "Today Is the Day, Part 1" or "Today Is the Day (1)" -> "Today Is the Day, Part 1" + */ + public static String replacePart(String self) { + return replacePart(self, ""); + } + + public static String replacePart(String self, String replacement) { + // handle '(n)', '(Part n)' and ': Part n' like syntax + String[] patterns = new String[] { "\\s*[(](\\w+)[)]$", "\\W+Part (\\w+)\\W*$" }; + + for (String pattern : patterns) { + Matcher matcher = compile(pattern, CASE_INSENSITIVE).matcher(self); + if (matcher.find()) { + return matcher.replaceAll(replacement).trim(); + } + } + + // no pattern matches, nothing to replace + return self; + } + + /** + * Apply ICU transliteration + * + * @see http://userguide.icu-project.org/transforms/general + */ + public static String transliterate(String self, String transformIdentifier) { + return Transliterator.getInstance(transformIdentifier).transform(self); + } + + /** + * Convert Unicode to ASCII as best as possible. Works with most alphabets/scripts used in the world. + * + * e.g. "Österreich" -> "Osterreich" "カタカナ" -> "katakana" + */ + public static String ascii(String self) { + return ascii(self, " "); + } + + public static String ascii(String self, String fallback) { + return Transliterator.getInstance("Any-Latin;Latin-ASCII;[:Diacritic:]remove").transform(self).replaceAll("[^\\p{ASCII}]+", fallback).trim(); + } + + /** + * Replace multiple replacement pairs + * + * e.g. replace('ä', 'ae', 'ö', 'oe', 'ü', 'ue') + */ + public static String replace(String self, String tr0, String tr1, String... tr) { + // the first two parameters are required, the rest of the parameter sequence is optional + self = self.replace(tr0, tr1); + + for (int i = 0; i < tr.length - 1; i += 2) { + String t = tr[i]; + String r = tr[i + 1]; + self = self.replace(t, r); + } + + return self; + } + + /** + * File utilities + */ + public static File getRoot(File self) { + return FileUtilities.listPath(self).get(0); + } + + public static List getPathList(File self) { + return FileUtilities.listPath(self); + } + + public static File getRelativePathTail(File self, int tailSize) { + return FileUtilities.getRelativePathTail(self, tailSize); + } + + public static long getDiskSpace(File self) { + List list = FileUtilities.listPath(self); + for (int i = list.size() - 1; i >= 0; i--) { + if (list.get(i).exists()) { + long usableSpace = list.get(i).getUsableSpace(); + if (usableSpace > 0) { + return usableSpace; + } + } + } + return 0; + } + }