package abbot.script;
import java.awt.Component;
import java.lang.reflect.Array;
import java.util.*;
import abbot.*;
import abbot.Log;
import abbot.i18n.Strings;
import abbot.finder.*;
import abbot.script.parsers.Parser;
import abbot.tester.*;
/** Provide parsing of a String into an array of appropriately typed
* arguments. Arrays are indicated by square brackets, and arguments are
* separated by commas, e.g.<br>
* <ul>
* <li>An empty String array (length zero): "[]"
* <li>Three arguments "one,two,three"
* <li>An array of length three: "[one,two,three]"
* <li>A single-element array of integer: "[1]"
* <li>A single null argument: "null"
* <li>An array of two strings: "[one,two]"
* <li>Commas must be escaped when they would otherwise be interpreted as an
* argument separator:<br>
* "one,two%2ctwo,three" (2nd argument is "two,two")
*/
public class ArgumentParser {
private ArgumentParser() { }
private static final String ESC_ESC_COMMA = "%%2C";
public static final String ESC_COMMA = "%2c";
public static final String NULL = "null";
public static final String DEFAULT_TOSTRING = "<default-tostring>";
/** Maps class names to their corresponding string parsers. */
private static Map parsers = new HashMap();
private static boolean isExtension(String name) {
return name.indexOf(".extensions.") != -1;
}
private static Parser findParser(String name, Class targetClass) {
Log.debug("Trying " + name + " for " + targetClass);
try {
Class cvtClass = isExtension(name)
? Class.forName(name, true, targetClass.getClassLoader())
: Class.forName(name);
Parser parser = (Parser)cvtClass.newInstance();
if (cvtClass.getName().indexOf(".extensions.") == -1)
parsers.put(targetClass, parser);
return parser;
}
catch(InstantiationException ie) {
Log.debug(ie);
}
catch(IllegalAccessException iae) {
Log.debug(iae);
}
catch(ClassNotFoundException cnf) {
Log.debug(cnf);
}
return null;
}
/** Set the parser for a given class. Returns the old one, if any. */
public static Parser setParser(Class cls, Parser parser) {
Parser old = (Parser)parsers.get(cls);
parsers.put(cls, parser);
return old;
}
/** Find a string parser for the given class. Returns null if none
* found.
*/
public static Parser getParser(Class cls) {
Parser parser = (Parser)parsers.get(cls);
// Load core testers with the current framework's class loader
// context, and anything else in the context of the code under test
if (parser == null) {
String base = ComponentTester.simpleClassName(cls);
String pkg = Parser.class.getPackage().getName();
parser = findParser(pkg + "." + base + "Parser", cls);
if (parser == null) {
parser = findParser(pkg + ".extensions."
+ base + "Parser", cls);
}
}
return parser;
}
private static boolean isBounded(String s) {
return s.startsWith("[") && s.endsWith("]")
|| s.startsWith("\"") && s.endsWith("\"")
|| s.startsWith("'") && s.endsWith("'");
}
private static String escapeCommas(String s) {
return replace(replace(s, ESC_COMMA, ESC_ESC_COMMA), ",", ESC_COMMA);
}
private static String unescapeCommas(String s) {
return replace(replace(s, ESC_COMMA, ","), ESC_ESC_COMMA, ESC_COMMA);
}
public static String encodeArguments(String[] args) {
StringBuffer sb = new StringBuffer();
if (args.length > 0) {
if (isBounded(args[0])) {
sb.append(args[0]);
}
else {
sb.append(escapeCommas(args[0]));
}
for (int i=1;i < args.length;i++) {
sb.append(",");
if (isBounded(args[i])) {
sb.append(args[i]);
}
else {
sb.append(escapeCommas(args[i]));
}
}
}
return sb.toString();
}
private static class Tokenizer extends ArrayList {
private static final long serialVersionUID = 1L;
public Tokenizer(String input) {
while (true) {
int index = input.indexOf(",");
if (index == -1) {
add(input);
break;
}
add(input.substring(0, index));
input = input.substring(index + 1);
}
}
}
/** Convert the given encoded String into an array of Strings.
* Interprets strings of the format "[el1,el2,el3]" to be a single (array)
* argument (such commas do not need escaping). <p>
* Explicit commas and square brackets in arguments must be escaped by
* preceding the character with a backslash ('\'). The strings
* '(null)' and 'null' are interpreted as the value null.<p>
* Explicit spaces should be protected by double quotes, e.g.
* " an argument bounded by spaces ".
*/
public static String[] parseArgumentList(String encodedArgs) {
ArrayList alist = new ArrayList();
if (encodedArgs == null || "".equals(encodedArgs))
return new String[0];
// handle old method of escaped commas
encodedArgs = replace(encodedArgs, "\\,", ESC_COMMA);
Iterator iter = new Tokenizer(encodedArgs).iterator();
while (iter.hasNext()) {
String str = (String)iter.next();
if (str.trim().startsWith("[")
&& !str.trim().endsWith("]")) {
while (iter.hasNext()) {
String next = (String)iter.next();
str += "," + next;
if (next.trim().endsWith("]")) {
break;
}
}
}
else if (str.trim().startsWith("\"")
&& !str.trim().endsWith("\"")) {
while (iter.hasNext()) {
String next = (String)iter.next();
str += "," + next;
if (next.trim().endsWith("\"")) {
break;
}
}
}
else if (str.trim().startsWith("'")
&& !str.trim().endsWith("'")) {
while (iter.hasNext()) {
String next = (String)iter.next();
str += "," + next;
if (next.trim().endsWith("'")) {
break;
}
}
}
if (NULL.equals(str.trim())) {
alist.add(null);
}
else {
// If it's an array, don't unescape the commas yet
if (!str.startsWith("[")) {
str = unescapeCommas(str);
}
alist.add(str);
}
}
return (String[])alist.toArray(new String[alist.size()]);
}
/** Performs property substitutions on the argument priort to evaluating
* it. Substitutions are not recursive.
*/
public static String substitute(Resolver resolver, String arg) {
if (arg == null) {
return arg;
}
int i = 0;
int marker = 0;
StringBuffer sb = new StringBuffer();
while ((i = arg.indexOf("${", marker)) != -1) {
if (marker < i) {
sb.append(arg.substring(marker, i));
marker = i;
}
int end = arg.indexOf("}", i);
if (end == -1) {
break;
}
String name = arg.substring(i + 2, end);
Object value = resolver.getProperty(name);
if (value == null) {
value = System.getProperty(name);
}
if (value == null) {
value = arg.substring(i, end + 1);
}
sb.append(toString(value));
marker = end + 1;
}
sb.append(arg.substring(marker));
return sb.toString();
}
/** Convert the given string into the given class, if possible,
* using any available parsers if conversion to basic types fails.
* The Resolver could be a parser, but it would need to adapt
* automatically to whatever is the current context.<p>
* Performs property substitution on the argument prior to evaluating it.
* Spaces are only trimmed from the argument if spaces have no meaning for
* the target class.
*/
public static Object eval(Resolver resolver, String arg, Class cls)
throws IllegalArgumentException,
NoSuchReferenceException,
ComponentSearchException {
// Perform property substitution
arg = substitute(resolver, arg);
Parser parser;
Object result = null;
try {
if (arg == null || arg.equals(NULL)) {
result = null;
}
else if (cls.equals(Boolean.class)
|| cls.equals(boolean.class)) {
result = Boolean.valueOf(arg.trim());
}
else if (cls.equals(Short.class)
|| cls.equals(short.class)) {
result = Short.valueOf(arg.trim());
}
else if (cls.equals(Integer.class)
|| cls.equals(int.class)) {
result = Integer.valueOf(arg.trim());
}
else if (cls.equals(Long.class)
|| cls.equals(long.class)) {
result = Long.valueOf(arg.trim());
}
else if (cls.equals(Float.class)
|| cls.equals(float.class)) {
result = Float.valueOf(arg.trim());
}
else if (cls.equals(Double.class)
|| cls.equals(double.class)) {
result = Double.valueOf(arg.trim());
}
else if (cls.equals(ComponentReference.class)) {
ComponentReference ref =
resolver.getComponentReference(arg.trim());
if (ref == null)
throw new NoSuchReferenceException("The resolver "
+ resolver
+ " has no reference '"
+ arg + "'");
result = ref;
}
else if (Component.class.isAssignableFrom(cls)) {
ComponentReference ref =
resolver.getComponentReference(arg.trim());
if (ref == null)
throw new NoSuchReferenceException("The resolver "
+ resolver
+ " has no reference '"
+ arg + "'");
// Avoid requiring the user to wait for a component to become
// available, in most cases. In those cases where the
// component creation is particularly slow, an explicit wait
// can be added.
// Note that this is not necessarily a wait for the component
// to become visible, since menu items are not normally
// visible even if they're available.
result = waitForComponentAvailable(ref);
}
else if (cls.equals(String.class)) {
result = arg;
}
else if (cls.isArray() && arg.trim().startsWith("[")) {
arg = arg.trim();
String[] args =
parseArgumentList(arg.substring(1, arg.length()-1));
Class base = cls.getComponentType();
Object arr = Array.newInstance(base, args.length);
for (int i=0;i < args.length;i++) {
Object obj = eval(resolver, args[i], base);
Array.set(arr, i, obj);
}
result = arr;
}
else if ((parser = getParser(cls)) != null) {
result = parser.parse(arg.trim());
}
else {
String msg = Strings.get("parser.conversion_error",
new Object[] {
arg.trim(),
cls.getName()
});
throw new IllegalArgumentException(msg);
}
return result;
}
catch(NumberFormatException nfe) {
String msg = Strings.get("parser.conversion_error",
new Object[] {
arg.trim(),
cls.getName()
});
throw new IllegalArgumentException(msg);
}
}
/** Evaluate the given set of arguments into the given set of types. */
public static Object[] eval(Resolver resolver,
String[] args, Class[] params)
throws IllegalArgumentException,
NoSuchReferenceException,
ComponentSearchException {
Object[] plist = new Object[params.length];
for(int i=0;i < plist.length;i++) {
plist[i] = eval(resolver, args[i], params[i]);
}
return plist;
}
/** Replace all instances in the given String of s1 with s2. */
public static String replace(String str, String s1, String s2) {
StringBuffer sb = new StringBuffer(str);
int index = 0;
while ((index = sb.toString().indexOf(s1, index)) != -1) {
sb.delete(index, index + s1.length());
sb.insert(index, s2);
index += s2.length();
}
return sb.toString();
}
// TODO: move this somewhere more appropriate; make public static, maybe
// in ComponentReference
private static Component waitForComponentAvailable(final ComponentReference ref)
throws ComponentSearchException {
try {
ComponentTester tester =
ComponentTester.getTester(Component.class);
tester.wait(new Condition() {
public boolean test() {
try { ref.getComponent(); }
catch(ComponentNotFoundException e) { return false; }
catch(MultipleComponentsFoundException m) { }
return true;
}
public String toString() {
return ref + " to become available";
}
}, ComponentTester.componentDelay);
}
catch(WaitTimedOutError wto) {
String msg = "Could not find " + ref + ": "
+ Step.toXMLString(ref);
throw new ComponentNotFoundException(msg);
}
return ref.getComponent();
}
/** Convert a value into a String representation. Handles null values and
arrays. Returns null if the String representation is the default
class@pointer format.
*/
public static String toString(Object value) {
if (value == null)
return NULL;
if (value.getClass().isArray()) {
StringBuffer sb = new StringBuffer();
sb.append("[");
for (int i=0;i < Array.getLength(value);i++) {
Object o = Array.get(value, i);
if (i > 0)
sb.append(",");
sb.append(toString(o));
}
sb.append("]");
return sb.toString();
}
String s = value.toString();
if (s == null)
return NULL;
if (isDefaultToString(s))
return DEFAULT_TOSTRING;
return s;
}
/** Returns whether the given String is the default toString()
* implementation for the given Object.
*/
public static boolean isDefaultToString(String s) {
if (s == null)
return false;
int at = s.indexOf("@");
if (at != -1) {
String hash = s.substring(at + 1, s.length());
try {
Integer.parseInt(hash, 16);
return true;
}
catch(NumberFormatException e) {
}
}
return false;
}
}