+ use Groovy instead of JavaScript in ExpressionFormat

This commit is contained in:
Reinhard Pointner 2009-09-22 21:23:39 +00:00
parent 384486631a
commit b04f89b7fd
9 changed files with 287 additions and 297 deletions

View File

@ -92,12 +92,9 @@
<include name="com/sun/jna/**" /> <include name="com/sun/jna/**" />
</zipfileset> </zipfileset>
<zipfileset src="${dir.lib}/js-engine.jar"> <zipfileset src="${dir.lib}/groovy.jar">
<include name="com/sun/phobos/script/**" /> <include name="groovy*/**" />
</zipfileset> <include name="org/codehaus/groovy/**" />
<zipfileset src="${dir.lib}/js.jar">
<include name="org/mozilla/**" />
</zipfileset> </zipfileset>
<zipfileset src="${dir.lib}/sublight-ws.jar"> <zipfileset src="${dir.lib}/sublight-ws.jar">

BIN
lib/groovy.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,6 +2,9 @@
package net.sourceforge.filebot.format; package net.sourceforge.filebot.format;
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.AbstractSet; import java.util.AbstractSet;
import java.util.HashMap; import java.util.HashMap;
@ -10,10 +13,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import org.mozilla.javascript.Scriptable;
public class AssociativeScriptObject implements GroovyObject {
public class AssociativeScriptObject implements Scriptable {
private final Map<String, Object> properties; private final Map<String, Object> properties;
@ -23,24 +24,14 @@ public class AssociativeScriptObject implements Scriptable {
} }
/**
* Defines properties available by name.
*
* @param name the name of the property
* @param start the object where lookup began
*/
public boolean has(String name, Scriptable start) {
return properties.containsKey(name);
}
/** /**
* Get the property with the given name. * Get the property with the given name.
* *
* @param name the property name * @param name the property name
* @param start the object where the lookup began * @param start the object where the lookup began
*/ */
public Object get(String name, Scriptable start) { @Override
public Object getProperty(String name) {
Object value = properties.get(name); Object value = properties.get(name);
if (value == null) if (value == null)
@ -50,52 +41,28 @@ public class AssociativeScriptObject implements Scriptable {
} }
/**
* Defines properties available by index.
*
* @param index the index of the property
* @param start the object where lookup began
*/
public boolean has(int index, Scriptable start) {
// get property by index not supported
return false;
}
/**
* Get property by index.
*
* @param index the index of the property
* @param start the object where the lookup began
*/
public Object get(int index, Scriptable start) {
// get property by index not supported
throw new BindingException(String.valueOf(index), "undefined");
}
/**
* Get property names.
*/
public Object[] getIds() {
return properties.keySet().toArray();
}
/**
* Returns the name of this JavaScript class.
*/
public String getClassName() {
return getClass().getSimpleName();
}
/**
* Returns the string value of this object.
*/
@Override @Override
public Object getDefaultValue(Class<?> typeHint) { public void setProperty(String name, Object value) {
return this.toString(); // ignore, object is immutable
}
@Override
public Object invokeMethod(String name, Object args) {
// ignore, object is merely a structure
return null;
}
@Override
public MetaClass getMetaClass() {
return null;
}
@Override
public void setMetaClass(MetaClass clazz) {
// ignore, don't care about MetaClass
} }
@ -106,51 +73,6 @@ public class AssociativeScriptObject implements Scriptable {
} }
public void put(String name, Scriptable start, Object value) {
// ignore, object is immutable
}
public void put(int index, Scriptable start, Object value) {
// ignore, object is immutable
}
public void delete(String id) {
// ignore, object is immutable
}
public void delete(int index) {
// ignore, object is immutable
}
public Scriptable getPrototype() {
return null;
}
public void setPrototype(Scriptable prototype) {
// ignore, don't care about prototype
}
public Scriptable getParentScope() {
return null;
}
public void setParentScope(Scriptable parent) {
// ignore, don't care about scope
}
public boolean hasInstance(Scriptable value) {
return false;
}
/** /**
* Map allowing look-up of values by a fault-tolerant key as specified by the defining key. * Map allowing look-up of values by a fault-tolerant key as specified by the defining key.
* *

View File

@ -1,142 +0,0 @@
// System, Math, Integer, etc.
importPackage(java.lang);
// Collection, Scanner, Random, UUID, etc.
importPackage(java.util);
// other useful classes
importClass(net.sourceforge.filebot.similarity.SeriesNameMatcher);
importClass(net.sourceforge.filebot.similarity.SeasonEpisodeMatcher);
/**
* Convenience methods for String.toLowerCase() and String.toUpperCase().
*/
String.prototype.lower = String.prototype.toLowerCase;
String.prototype.upper = String.prototype.toUpperCase;
/**
* Pad strings or numbers with given characters ('0' by default).
*
* e.g. "1" -> "01"
*/
String.prototype.pad = Number.prototype.pad = function(length, padding) {
var s = this.toString();
// use default padding, if padding is undefined or empty
var p = padding ? padding.toString() : '0';
while (s.length < length) {
s = p + s;
}
return s;
}
/**
* Replace space characters with a given characters.
*
* e.g. "Doctor Who" -> "Doctor_Who"
*/
String.prototype.space = function(replacement) {
return this.replace(/\s+/g, replacement);
}
/**
* Upper-case all initials.
*
* e.g. "The Day a new Demon was born" -> "The Day A New Demon Was Born"
*/
String.prototype.upperInitial = function() {
return this.replace(/\b[a-z]/g, function(letter) { return letter.toUpperCase() });
}
/**
* Lower-case all letters that are not initials.
*
* e.g. "Gundam SEED" -> "Gundam Seed"
*/
String.prototype.lowerTrail = function() {
return this.replace(/\b([a-z])([a-z]+)\b/gi, function(match, initial, trail) { return initial + trail.toLowerCase() });
}
/**
* Remove leading and trailing whitespace.
*/
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, "");
}
/**
* Return substring before the given delimiter.
*/
String.prototype.before = function(delimiter) {
var endIndex = delimiter instanceof RegExp ? this.search(delimiter) : this.indexOf(delimiter);
// delimiter was found, return leading substring, else return original value
return endIndex >= 0 ? this.substring(0, endIndex) : this;
}
/**
* Return substring after the given delimiter.
*/
String.prototype.after = function(delimiter) {
if (delimiter instanceof RegExp) {
var match = this.match(delimiter);
if (match == null)
return this;
// use pattern match as delimiter
delimiter = match[0];
}
var startIndex = this.indexOf(delimiter);
// delimiter was found, return trailing substring, else return original value
return startIndex >= 0 ? this.substring(startIndex + delimiter.length, this.length) : this;
}
/**
* Replace trailing parenthesis including any leading whitespace.
*
* e.g. "The IT Crowd (UK)" -> "The IT Crowd"
*/
String.prototype.replaceTrailingBraces = function(replacement) {
// use empty string as default replacement
var r = replacement ? replacement : "";
return this.replace(/\s*[(]([^)]*)[)]$/, r);
}
/**
* Replace 'part section'.
*
* e.g. "Today Is the Day: Part 1" -> "Today Is the Day, Part 1"
* "Today Is the Day (1)" -> "Today Is the Day, Part 1"
*/
String.prototype.replacePart = function (replacement) {
// use empty string as default replacement
var r = replacement ? replacement : "";
// handle '(n)', '(Part n)' and ': Part n' like syntax
var pattern = [/\s*[(](\w+)[)]$/i, /\W*Part (\w+)\W*$/i];
for (var i = 0; i < pattern.length; i++) {
if (pattern[i].test(this)) {
return this.replace(pattern[i], r);
}
}
// no pattern matches, nothing to replace
return this;
}

View File

@ -2,6 +2,10 @@
package net.sourceforge.filebot.format; package net.sourceforge.filebot.format;
import static net.sourceforge.tuned.ExceptionUtilities.*;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MissingPropertyException;
import java.io.FilePermission; import java.io.FilePermission;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.security.AccessControlContext; import java.security.AccessControlContext;
@ -18,8 +22,6 @@ import java.text.ParsePosition;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.PropertyPermission; import java.util.PropertyPermission;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.Bindings; import javax.script.Bindings;
import javax.script.Compilable; import javax.script.Compilable;
@ -29,9 +31,8 @@ import javax.script.ScriptEngine;
import javax.script.ScriptException; import javax.script.ScriptException;
import javax.script.SimpleScriptContext; import javax.script.SimpleScriptContext;
import org.mozilla.javascript.EcmaError; import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.codehaus.groovy.jsr223.GroovyScriptEngineFactory;
import com.sun.phobos.script.javascript.RhinoScriptEngine;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
@ -52,10 +53,10 @@ public class ExpressionFormat extends Format {
protected ScriptEngine initScriptEngine() throws ScriptException { protected ScriptEngine initScriptEngine() throws ScriptException {
// don't use jdk rhino so we can use rhino specific features and classes (e.g. Scriptable) // use groovy script engine
ScriptEngine engine = new RhinoScriptEngine(); ScriptEngine engine = new GroovyScriptEngineFactory().getScriptEngine();
engine.eval(new InputStreamReader(ExpressionFormat.class.getResourceAsStream("ExpressionFormat.global.js"))); engine.eval(new InputStreamReader(ExpressionFormat.class.getResourceAsStream("ExpressionFormat.lib.groovy")));
return engine; return engine;
} }
@ -69,29 +70,71 @@ public class ExpressionFormat extends Format {
protected Object[] compile(String expression, Compilable engine) throws ScriptException { protected Object[] compile(String expression, Compilable engine) throws ScriptException {
List<Object> compilation = new ArrayList<Object>(); List<Object> compilation = new ArrayList<Object>();
Matcher matcher = Pattern.compile("\\{([^\\{]*?)\\}").matcher(expression); char open = '{';
char close = '}';
int position = 0; StringBuilder token = new StringBuilder();
int level = 0;
while (matcher.find()) { // parse expressions and literals
if (position < matcher.start()) { for (int i = 0; i < expression.length(); i++) {
// literal before char c = expression.charAt(i);
compilation.add(expression.substring(position, matcher.start()));
if (c == open) {
if (level == 0) {
if (token.length() > 0) {
compilation.add(token.toString());
token.setLength(0);
}
} else {
token.append(c);
}
level++;
} else if (c == close) {
if (level == 1) {
if (token.length() > 0) {
try {
compilation.add(engine.compile(token.toString()));
} 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);
}
}
} else {
token.append(c);
}
level--;
} else {
token.append(c);
} }
String script = matcher.group(1); // sanity check
if (level < 0) {
if (script.length() > 0) { throw new ScriptException("SyntaxError: unexpected token: " + close);
// compiled script, or literal
compilation.add(engine.compile(script));
} }
position = matcher.end();
} }
if (position < expression.length()) { // sanity check
// tail if (level != 0) {
compilation.add(expression.substring(position, expression.length())); throw new ScriptException("SyntaxError: missing token: " + close);
}
// append tail
if (token.length() > 0) {
compilation.add(token.toString());
} }
return compilation.toArray(); return compilation.toArray();
@ -129,16 +172,7 @@ public class ExpressionFormat extends Format {
sb.append(value); sb.append(value);
} }
} catch (ScriptException e) { } catch (ScriptException e) {
EcmaError ecmaError = ExceptionUtilities.findCause(e, EcmaError.class); handleException(e);
// 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 { } else {
sb.append(snipped); sb.append(snipped);
@ -149,6 +183,17 @@ public class ExpressionFormat extends Format {
} }
protected void handleException(ScriptException exception) {
if (findCause(exception, MissingPropertyException.class) != null) {
lastException = new ExpressionException(new BindingException(findCause(exception, MissingPropertyException.class).getProperty(), "undefined", exception));
} else if (findCause(exception, GroovyRuntimeException.class) != null) {
lastException = new ExpressionException(findCause(exception, GroovyRuntimeException.class).getMessage(), exception);
} else {
lastException = exception;
}
}
public ScriptException caughtScriptException() { public ScriptException caughtScriptException() {
return lastException; return lastException;
} }

View File

@ -0,0 +1,90 @@
// Collection, Scanner, Random, UUID, etc.
import java.util.*
/**
* Convenience methods for String.toLowerCase()and String.toUpperCase()
*/
String.metaClass.lower = { toLowerCase() }
String.metaClass.upper = { toUpperCase() }
/**
* Pad strings or numbers with given characters ('0' by default).
*
* e.g. "1" -> "01"
*/
String.metaClass.pad = Number.metaClass.pad = { length = 2, padding = "0" -> delegate.toString().padLeft(length, padding) }
/**
* Replace space characters with a given characters.
*
* e.g. "Doctor Who" -> "Doctor_Who"
*/
String.metaClass.space = { replacement -> replaceAll(/\s+/, replacement) }
/**
* Upper-case all initials.
*
* e.g. "The Day a new Demon was born" -> "The Day A New Demon Was Born"
*/
String.metaClass.upperInitial = { replaceAll(/\b[a-z]/, { it.toUpperCase() }) }
/**
* Lower-case all letters that are not initials.
*
* e.g. "Gundam SEED" -> "Gundam Seed"
*/
String.metaClass.lowerTrail = { replaceAll(/\b(\p{Alpha})(\p{Alpha}+)\b/, { match, initial, trail -> initial + trail.toLowerCase() }) }
/**
* Return substring before the given delimiter.
*/
String.metaClass.before = {
def matcher = delegate =~ it
// delimiter was found, return leading substring, else return original value
return matcher.find() ? delegate.substring(0, matcher.start()) : delegate
}
/**
* Return substring after the given delimiter.
*/
String.metaClass.after = {
def matcher = delegate =~ it
// delimiter was found, return trailing substring, else return original value
return matcher.find() ? delegate.substring(matcher.end(), delegate.length()) : delegate
}
/**
* Replace trailing parenthesis including any leading whitespace.
*
* e.g. "The IT Crowd (UK)" -> "The IT Crowd"
*/
String.metaClass.replaceTrailingBraces = { replacement = "" -> replaceAll(/\s*[(]([^)]*)[)]$/, replacement) }
/**
* Replace 'part section'.
*
* e.g. "Today Is the Day: Part 1" -> "Today Is the Day, Part 1"
* "Today Is the Day (1)" -> "Today Is the Day, Part 1"
*/
String.metaClass.replacePart = { replacement = "" ->
// handle '(n)', '(Part n)' and ': Part n' like syntax
for (pattern in [/\s*[(](\w+)[)]$/, /(?i)\W*Part (\w+)\W*$/]) {
if ((delegate =~ pattern).find()) {
return replaceAll(pattern, replacement);
}
}
// no pattern matches, nothing to replace
return delegate;
}

View File

@ -21,8 +21,6 @@ public class ExpressionFormatTest {
Object[] expression = format.compile("name: {name}, number: {number}", (Compilable) format.initScriptEngine()); Object[] expression = format.compile("name: {name}, number: {number}", (Compilable) format.initScriptEngine());
assertEquals(4, expression.length, 0);
assertTrue(expression[0] instanceof String); assertTrue(expression[0] instanceof String);
assertTrue(expression[1] instanceof CompiledScript); assertTrue(expression[1] instanceof CompiledScript);
assertTrue(expression[2] instanceof String); assertTrue(expression[2] instanceof String);
@ -34,17 +32,97 @@ public class ExpressionFormatTest {
public void format() throws Exception { public void format() throws Exception {
assertEquals("X5-452", new TestScriptFormat("X5-{value}").format("452")); assertEquals("X5-452", new TestScriptFormat("X5-{value}").format("452"));
// test pad // padding
assertEquals("[007]", new TestScriptFormat("[{value.pad(3)}]").format("7")); assertEquals("[007]", new TestScriptFormat("[{value.pad(3)}]").format("7"));
assertEquals("[xx7]", new TestScriptFormat("[{value.pad(3, 'x')}]").format("7"));
// case
assertEquals("ALL_CAPS", new TestScriptFormat("{value.upper()}").format("all_caps"));
assertEquals("lower_case", new TestScriptFormat("{value.lower()}").format("LOWER_CASE"));
// normalize
assertEquals("Doctor_Who", new TestScriptFormat("{value.space('_')}").format("Doctor Who"));
assertEquals("The Day A New Demon Was Born", new TestScriptFormat("{value.upperInitial()}").format("The Day a new Demon was born"));
assertEquals("Gundam Seed", new TestScriptFormat("{value.lowerTrail()}").format("Gundam SEED"));
// substring
assertEquals("first", new TestScriptFormat("{value.before(/[^a-z]/)}").format("first|second"));
assertEquals("second", new TestScriptFormat("{value.after(/[^a-z]/)}").format("first|second"));
// replace trailing braces
assertEquals("The IT Crowd", new TestScriptFormat("{value.replaceTrailingBraces()}").format("The IT Crowd (UK)"));
// replace part
assertEquals("Today Is the Day, Part 1", new TestScriptFormat("{value.replacePart(', Part $1')}").format("Today Is the Day (1)"));
assertEquals("Today Is the Day, Part 1", new TestScriptFormat("{value.replacePart(', Part $1')}").format("Today Is the Day: part 1"));
// choice // choice
assertEquals("not to be", new TestScriptFormat("{if (value) 'to be'; else 'not to be'}").format(null)); assertEquals("not to be", new TestScriptFormat("{value ? 'to be' : 'not to be'}").format(null));
assertEquals("default", new TestScriptFormat("{value ?: 'default'}").format(null));
}
// empty choice
assertEquals("", new TestScriptFormat("{if (value) 'to be'}").format(null));
// loop @Test
assertEquals("0123456789", new TestScriptFormat("{var s=''; for (var i=0; i<parseInt(value);i++) s+=i;}").format("10")); public void closures() throws Exception {
assertEquals("[ant, cat]", new TestScriptFormat("{['ant', 'buffalo', 'cat', 'dinosaur'].findAll{ it.size() <= 3 }}").format(null));
}
@Test
public void illegalSyntax() throws Exception {
try {
// will throw exception
new TestScriptFormat("{value.}");
// exception must be thrown
fail("exception expected");
} catch (ScriptException e) {
// check message
assertEquals("SyntaxError: unexpected token: .", e.getMessage());
}
}
@Test
public void illegalClosingBracket() throws Exception {
try {
// will throw exception
new TestScriptFormat("{{ it -> 'value' }}}");
// exception must be thrown
fail("exception expected");
} catch (ScriptException e) {
// check message
assertEquals("SyntaxError: unexpected token: }", e.getMessage());
}
}
@Test
public void illegalBinding() throws Exception {
TestScriptFormat format = new TestScriptFormat("{xyz}");
format.format(new SimpleBindings());
// check message
assertEquals("BindingError: \"xyz\": undefined", format.caughtScriptException().getMessage());
}
@Test
public void illegalProperty() throws Exception {
TestScriptFormat format = new TestScriptFormat("{value.xyz}");
format.format("test");
// check message
assertEquals("BindingError: \"xyz\": undefined", format.caughtScriptException().getMessage());
}
@Test
public void illegalMethod() throws Exception {
TestScriptFormat format = new TestScriptFormat("{value.xyz()}");
format.format("test");
// check message
assertEquals("No signature of method: java.lang.String.xyz() is applicable for argument types: () values: []", format.caughtScriptException().getMessage());
} }