/*
* Copyright (C) 2009 JavaRosa
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.openrosa.client.jr.xpath.expr;
import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Vector;
import org.openrosa.client.java.io.DataInputStream;
import org.openrosa.client.java.io.DataOutputStream;
import org.openrosa.client.jr.core.model.condition.EvaluationContext;
import org.openrosa.client.jr.core.model.condition.IFunctionHandler;
import org.openrosa.client.jr.core.model.instance.FormInstance;
import org.openrosa.client.jr.core.model.instance.TreeReference;
import org.openrosa.client.jr.core.model.utils.DateUtils;
import org.openrosa.client.jr.core.util.MathUtils;
import org.openrosa.client.jr.core.util.externalizable.DeserializationException;
import org.openrosa.client.jr.core.util.externalizable.ExtUtil;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapListPoly;
import org.openrosa.client.jr.core.util.externalizable.PrototypeFactory;
import org.openrosa.client.jr.xpath.IExprDataType;
import org.openrosa.client.jr.xpath.XPathTypeMismatchException;
import org.openrosa.client.jr.xpath.XPathUnhandledException;
/**
* Representation of an xpath function expression.
*
* All of the built-in xpath functions are included here, as well as the xpath type conversion logic
*
* Evaluation of functions can delegate out to custom function handlers that must be registered at
* runtime.
*
* @author Drew Roos
*
*/
public class XPathFuncExpr extends XPathExpression {
public XPathQName id; //name of the function
public XPathExpression[] args; //argument list
public XPathFuncExpr () { } //for deserialization
public XPathFuncExpr (XPathQName id, XPathExpression[] args) {
this.id = id;
this.args = args;
}
public String toString () {
StringBuffer sb = new StringBuffer();
sb.append("{func-expr:");
sb.append(id.toString());
sb.append(",{");
for (int i = 0; i < args.length; i++) {
sb.append(args[i].toString());
if (i < args.length - 1)
sb.append(",");
}
sb.append("}}");
return sb.toString();
}
public boolean equals (Object o) {
if (o instanceof XPathFuncExpr) {
XPathFuncExpr x = (XPathFuncExpr)o;
//Shortcuts for very easily comprable values
if(!id.equals(x.id) || args.length != x.args.length) {
return false;
}
return ExtUtil.arrayEquals(args, x.args);
} else {
return false;
}
}
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
id = (XPathQName)ExtUtil.read(in, XPathQName.class);
Vector v = (Vector)ExtUtil.read(in, new ExtWrapListPoly(), pf);
args = new XPathExpression[v.size()];
for (int i = 0; i < args.length; i++)
args[i] = (XPathExpression)v.elementAt(i);
}
public void writeExternal(DataOutputStream out) throws IOException {
Vector v = new Vector();
for (int i = 0; i < args.length; i++)
v.addElement(args[i]);
ExtUtil.write(out, id);
ExtUtil.write(out, new ExtWrapListPoly(v));
}
/**
* Evaluate the function call.
*
* First check if the function is a member of the built-in function suite. If not, then check
* for any custom handlers registered to handler the function. If not, throw and exception.
*
* Both function name and appropriate arguments are taken into account when finding a suitable
* handler. For built-in functions, the number of arguments must match; for custom functions,
* the supplied arguments must match one of the function prototypes defined by the handler.
*
*/
public Object eval (FormInstance model, EvaluationContext evalContext) {
String name = id.toString();
Object[] argVals = new Object[args.length];
HashMap funcHandlers = evalContext.getFunctionHandlers();
for (int i = 0; i < args.length; i++) {
argVals[i] = args[i].eval(model, evalContext);
}
//check built-in functions
if (name.equals("true") && args.length == 0) {
return Boolean.TRUE;
} else if (name.equals("false") && args.length == 0) {
return Boolean.FALSE;
} else if (name.equals("boolean") && args.length == 1) {
return toBoolean(argVals[0]);
} else if (name.equals("number") && args.length == 1) {
return toNumeric(argVals[0]);
} else if (name.equals("int") && args.length == 1) { //non-standard
return toInt(argVals[0]);
} else if (name.equals("string") && args.length == 1) {
return toString(argVals[0]);
} else if (name.equals("date") && args.length == 1) { //non-standard
return toDate(argVals[0]);
} else if (name.equals("not") && args.length == 1) {
return boolNot(argVals[0]);
} else if (name.equals("boolean-from-string") && args.length == 1) {
return boolStr(argVals[0]);
} else if (name.equals("if") && args.length == 3) { //non-standard
return ifThenElse(argVals[0], argVals[1], argVals[2]);
} else if ((name.equals("selected") || name.equals("is-selected")) && args.length == 2) { //non-standard
return multiSelected(argVals[0], argVals[1]);
} else if (name.equals("count-selected") && args.length == 1) { //non-standard
return countSelected(argVals[0]);
} else if (name.equals("count") && args.length == 1) {
return count(argVals[0]);
} else if (name.equals("sum") && args.length == 1) {
return sum(model, argVals[0]);
} else if (name.equals("today") && args.length == 0) {
return DateUtils.roundDate(new Date());
} else if (name.equals("now") && args.length == 0) {
return new Date();
} else if (name.equals("concat")) {
if (args.length == 1 && argVals[0] instanceof Vector) {
return join("", nodesetToArgList(model, (Vector)argVals[0]));
} else {
return join("", argVals);
}
} else if (name.equals("join") && args.length >= 1) {
if (args.length == 2 && argVals[1] instanceof Vector) {
return join(argVals[0], nodesetToArgList(model, (Vector)argVals[1]));
} else {
return join(argVals[0], subsetArgList(argVals, 1));
}
} else if (name.equals("checklist") && args.length >= 2) { //non-standard
if (args.length == 3 && argVals[2] instanceof Vector) {
return checklist(argVals[0], argVals[1], nodesetToArgList(model, (Vector)argVals[2]));
} else {
return checklist(argVals[0], argVals[1], subsetArgList(argVals, 2));
}
} else if (name.equals("weighted-checklist") && args.length >= 2 && args.length % 2 == 0) { //non-standard
if (args.length == 4 && argVals[2] instanceof Vector && argVals[3] instanceof Vector) {
Object[] factors = nodesetToArgList(model, (Vector)argVals[2]);
Object[] weights = nodesetToArgList(model, (Vector)argVals[3]);
if (factors.length != weights.length) {
throw new XPathTypeMismatchException("weighted-checklist: nodesets not same length");
}
return checklistWeighted(argVals[0], argVals[1], factors, weights);
} else {
return checklistWeighted(argVals[0], argVals[1], subsetArgList(argVals, 2, 2), subsetArgList(argVals, 3, 2));
}
} else if (name.equals("regex") && args.length == 2) { //non-standard
return regex(argVals[0], argVals[1]);
} else {
//check for custom handler
IFunctionHandler handler = (IFunctionHandler)funcHandlers.get(name);
if (handler != null) {
return evalCustomFunction(handler, argVals);
} else {
throw new XPathUnhandledException("function \'" + name + "\'");
}
}
}
/**
* Given a handler registered to handle the function, try to coerce the function arguments into
* one of the prototypes defined by the handler. If no suitable prototype found, throw an eval
* exception. Otherwise, evaluate.
*
* Note that if the handler supports 'raw args', it will receive the full, unaltered argument
* list if no prototype matches. (this lets functions support variable-length argument lists)
*
* @param handler
* @param args
* @return
*/
private static Object evalCustomFunction (IFunctionHandler handler, Object[] args) {
Vector prototypes = handler.getPrototypes();
Enumeration e = prototypes.elements();
Object[] typedArgs = null;
while (typedArgs == null && e.hasMoreElements()) {
typedArgs = null; //matchPrototype(args, (Class[])e.nextElement());
}
if (typedArgs != null) {
return handler.eval(typedArgs);
} else if (handler.rawArgs()) {
return handler.eval(args); //should we have support for expanding nodesets here?
} else {
throw new XPathTypeMismatchException("for function \'" + handler.getName() + "\'");
}
}
/******** HANDLERS FOR BUILT-IN FUNCTIONS ********
*
* the functions below are the handlers for the built-in xpath function suite
*
* if you add a function to the suite, it should adhere to the following pattern:
*
* * the function takes in its arguments as objects (DO NOT cast the arguments when calling
* the handler up in eval() (i.e., return stringLength((String)argVals[0]) <--- NO!)
*
* * the function converts the generic argument(s) to the desired type using the built-in
* xpath type conversion functions (toBoolean(), toNumeric(), toString(), toDate())
*
* * the function MUST return an object of type Boolean, Double, String, or Date; it may
* never return null (instead return the empty string or NaN)
*
* * the function may throw exceptions, but should try as hard as possible not to, and if
* it must, strive to make it an XPathException
*
*/
/**
* convert a value to a boolean using xpath's type conversion rules
*
* @param o
* @return
*/
public static Boolean toBoolean (Object o) {
Boolean val = null;
if (o instanceof Boolean) {
val = (Boolean)o;
} else if (o instanceof Double) {
double d = ((Double)o).doubleValue();
val = new Boolean(Math.abs(d) > 1.0e-12 && !Double.isNaN(d));
} else if (o instanceof String) {
String s = (String)o;
val = new Boolean(s.length() > 0);
} else if (o instanceof Date) {
val = Boolean.TRUE;
} else if (o instanceof Vector) {
return new Boolean(count(o).doubleValue() > 0);
} else if (o instanceof IExprDataType) {
val = ((IExprDataType)o).toBoolean();
}
if (val != null) {
return val;
} else {
throw new XPathTypeMismatchException("converting to boolean");
}
}
/**
* convert a value to a number using xpath's type conversion rules (note that xpath itself makes
* no distinction between integer and floating point numbers)
*
* @param o
* @return
*/
public static Double toNumeric (Object o) {
Double val = null;
if (o instanceof Boolean) {
val = new Double(((Boolean)o).booleanValue() ? 1 : 0);
} else if (o instanceof Double) {
val = (Double)o;
} else if (o instanceof String) {
/* annoying, but the xpath spec doesn't recognize scientific notation, or +/-Infinity
* when converting a string to a number
*/
String s = (String)o;
double d;
try {
s = s.trim();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c != '-' && c != '.' && (c < '0' || c > '9'))
throw new NumberFormatException();
}
d = Double.parseDouble(s);
val = new Double(d);
} catch (NumberFormatException nfe) {
val = new Double(Double.NaN);
}
} else if (o instanceof Date) {
val = new Double(DateUtils.daysSinceEpoch((Date)o));
} else if (o instanceof IExprDataType) {
val = ((IExprDataType)o).toNumeric();
}
if (val != null) {
return val;
} else {
throw new XPathTypeMismatchException("converting to numeric");
}
}
/**
* convert a number to an integer by truncating the fractional part. if non-numeric, coerce the
* value to a number first. note that the resulting return value is still a Double, as required
* by the xpath engine
*
* @param o
* @return
*/
public static Double toInt (Object o) {
Double val = toNumeric(o);
if (val.isInfinite() || val.isNaN()) {
return val;
} else if (val.doubleValue() >= Long.MAX_VALUE || val.doubleValue() <= Long.MIN_VALUE) {
return val;
} else {
long l = val.longValue();
Double dbl = new Double(l);
if (l == 0 && (val.doubleValue() < 0. || val.equals(new Double(-0.)))) {
dbl = new Double(-0.);
}
return dbl;
}
}
/**
* convert a value to a string using xpath's type conversion rules
*
* @param o
* @return
*/
public static String toString (Object o) {
String val = null;
if (o instanceof Boolean) {
val = (((Boolean)o).booleanValue() ? "true" : "false");
} else if (o instanceof Double) {
double d = ((Double)o).doubleValue();
if (Double.isNaN(d)) {
val = "NaN";
} else if (Math.abs(d) < 1.0e-12) {
val = "0";
} else if (Double.isInfinite(d)) {
val = (d < 0 ? "-" : "") + "Infinity";
} else if (Math.abs(d - (int)d) < 1.0e-12) {
val = String.valueOf((int)d);
} else {
val = String.valueOf(d);
}
} else if (o instanceof String) {
val = (String)o;
} else if (o instanceof Date) {
val = DateUtils.formatDate((Date)o, DateUtils.FORMAT_ISO8601);
} else if (o instanceof IExprDataType) {
val = ((IExprDataType)o).toString();
}
if (val != null) {
return val;
} else {
throw new XPathTypeMismatchException("converting to string");
}
}
/**
* convert a value to a date. note that xpath has no intrinsic representation of dates, so this
* is off-spec. dates convert to strings as 'yyyy-mm-dd', convert to numbers as # of days since
* the unix epoch, and convert to booleans always as 'true'
*
* string and int conversions are reversable, however:
* * cannot convert bool to date
* * empty string and NaN (xpath's 'null values') go unchanged, instead of being converted
* into a date (which would cause an error, since Date has no null value (other than java
* null, which the xpath engine can't handle))
* * note, however, than non-empty strings that aren't valid dates _will_ cause an error
* during conversion
*
* @param o
* @return
*/
public static Object toDate (Object o) {
if (o instanceof Double) {
Double n = toInt(o);
if (n.isNaN()) {
return n;
}
if (n.isInfinite() || n.doubleValue() > Integer.MAX_VALUE || n.doubleValue() < Integer.MIN_VALUE) {
throw new XPathTypeMismatchException("converting out-of-range value to date");
}
return DateUtils.dateAdd(DateUtils.getDate(1970, 1, 1), n.intValue());
} else if (o instanceof String) {
String s = (String)o;
if (s.length() == 0) {
return s;
}
Date d = DateUtils.parseDate(s);
if (d == null) {
throw new XPathTypeMismatchException("converting to date");
} else {
return d;
}
} else if (o instanceof Date) {
return DateUtils.roundDate((Date)o);
} else {
throw new XPathTypeMismatchException("converting to date");
}
}
public static Boolean boolNot (Object o) {
boolean b = toBoolean(o).booleanValue();
return new Boolean(!b);
}
public static Boolean boolStr (Object o) {
String s = toString(o);
if (s.equalsIgnoreCase("true") || s.equals("1"))
return Boolean.TRUE;
else
return Boolean.FALSE;
}
public static Object ifThenElse (Object o1, Object o2, Object o3) {
boolean b = toBoolean(o1).booleanValue();
return (b ? o2 : o3);
}
/**
* return whether a particular choice of a multi-select is selected
*
* @param o1 XML-serialized answer to multi-select question (i.e, space-delimited choice values)
* @param o2 choice to look for
* @return
*/
public static Boolean multiSelected (Object o1, Object o2) {
String s1 = (String)o1;
String s2 = ((String)o2).trim();
return new Boolean((" " + s1 + " ").indexOf(" " + s2 + " ") != -1);
}
/**
* return the number of choices in a multi-select answer
*
* @param o XML-serialized answer to multi-select question (i.e, space-delimited choice values)
* @return
*/
public static Double countSelected (Object o) {
String s = (String)o;
return new Double(DateUtils.split(s, " ", true).size());
}
/**
* count the number of nodes in a nodeset
*
* @param o
* @return
*/
public static Double count (Object o) {
if (o instanceof Vector) {
return new Double(((Vector)o).size());
} else {
throw new XPathTypeMismatchException("not a nodeset");
}
}
/**
* sum the values in a nodeset; each element is coerced to a numeric value
*
* @param model
* @param o
* @return
*/
public static Double sum (FormInstance model, Object o) {
if (o instanceof Vector) {
Vector v = (Vector)o;
double sum = 0.0;
for (int i = 0; i < v.size(); i++) {
TreeReference ref = (TreeReference)v.elementAt(i);
sum += toNumeric(XPathPathExpr.getRefValue(model, ref)).doubleValue();
}
return new Double(sum);
} else {
throw new XPathTypeMismatchException("not a nodeset");
}
}
/**
* concatenate an abritrary-length argument list of string values together
*
* @param argVals
* @return
*/
public static String join (Object oSep, Object[] argVals) {
String sep = toString(oSep);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < argVals.length; i++) {
sb.append(toString(argVals[i]));
if (i < argVals.length - 1)
sb.append(sep);
}
return sb.toString();
}
/**
* perform a 'checklist' computation, enabling expressions like 'if there are at least 3 risk
* factors active'
*
* @param argVals
* the first argument is a numeric value expressing the minimum number of factors required.
* if -1, no minimum is applicable
* the second argument is a numeric value expressing the maximum number of allowed factors.
* if -1, no maximum is applicalbe
* arguments 3 through the end are the individual factors, each coerced to a boolean value
* @return true if the count of 'true' factors is between the applicable minimum and maximum,
* inclusive
*/
public static Boolean checklist (Object oMin, Object oMax, Object[] factors) {
int min = toNumeric(oMin).intValue();
int max = toNumeric(oMax).intValue();
int count = 0;
for (int i = 0; i < factors.length; i++) {
if (toBoolean(factors[i]).booleanValue())
count++;
}
return new Boolean((min < 0 || count >= min) && (max < 0 || count <= max));
}
/**
* very similar to checklist, only each factor is assigned a real-number 'weight'.
*
* the first and second args are again the minimum and maximum, but -1 no longer means
* 'not applicable'.
*
* subsequent arguments come in pairs: first the boolean value, then the floating-point
* weight for that value
*
* the weights of all the 'true' factors are summed, and the function returns whether
* this sum is between the min and max
*
* @param argVals
* @return
*/
public static Boolean checklistWeighted (Object oMin, Object oMax, Object[] flags, Object[] weights) {
double min = toNumeric(oMin).doubleValue();
double max = toNumeric(oMax).doubleValue();
double sum = 0.;
for (int i = 0; i < flags.length; i++) {
boolean flag = toBoolean(flags[i]).booleanValue();
double weight = toNumeric(weights[i]).doubleValue();
if (flag)
sum += weight;
}
return new Boolean(sum >= min && sum <= max);
}
/**
* determine if a string matches a regular expression.
*
* @param o1 string being matched
* @param o2 regular expression
* @return
*/
public static Boolean regex (Object o1, Object o2) {
String str = toString(o1);
String re = toString(o2);
/*RE regexp = new RE(re);
boolean result = regexp.match(str);
return new Boolean(result);*/
return true; //?????????????
}
/**
* convert a nodeset argument into a standard argument list
*
* @param model
* @param nodeset
* @return
*/
private static Object[] nodesetToArgList (FormInstance model, Vector nodeset) {
Object[] args = new Object[nodeset.size()];
for (int i = 0; i < nodeset.size(); i++) {
TreeReference ref = (TreeReference)nodeset.elementAt(i);
Object val = XPathPathExpr.getRefValue(model, ref);
//sanity check
if (val == null) {
throw new RuntimeException("retrived a null value out of a nodeset! shouldn't happen!");
}
args[i] = val;
}
return args;
}
private static Object[] subsetArgList (Object[] args, int start) {
return subsetArgList(args, start, 1);
}
/**
* return a subset of an argument list as a new arguments list
*
* @param args
* @param start index to start at
* @param skip sub-list will contain every nth argument, where n == skip (default: 1)
* @return
*/
private static Object[] subsetArgList (Object[] args, int start, int skip) {
if (start > args.length || skip < 1) {
throw new RuntimeException("error in subsetting arglist");
}
Object[] subargs = new Object[(int)MathUtils.divLongNotSuck(args.length - start - 1, skip) + 1];
for (int i = start, j = 0; i < args.length; i += skip, j++) {
subargs[j] = args[i];
}
return subargs;
}
}