/**
* Copyright 2015 Santhosh Kumar Tekuri
*
* The JLibs authors license this file to you 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 jlibs.xml.sax.dog.expr.func;
import jlibs.xml.sax.dog.DataType;
import jlibs.xml.sax.dog.expr.Expression;
import jlibs.xml.sax.dog.path.LocationPath;
import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;
import java.util.*;
/**
* @author Santhosh Kumar T
*/
public class Functions{
public static Expression typeCast(Object current, DataType expected){
if(current instanceof Expression){
Expression expr = (Expression)current;
DataType exprResultType = expr.resultType;
if(exprResultType==expected || expected==DataType.STRINGS)
return expr;
if(expected==DataType.NUMBERS){
if(exprResultType==DataType.NUMBER)
return expr;
else
expected = DataType.NUMBER;
}
if(expected==DataType.PRIMITIVE){
switch(exprResultType){
case STRING:
case BOOLEAN:
case NUMBER:
return expr;
default:
expected = DataType.STRING;
}
}
FunctionCall function = new FunctionCall(new Functions.TypeCast(expected));
function.addValidMember(expr, 0);
return function.simplify();
}else
return ((LocationPath)current).typeCast(expected).simplify();
}
public static class TypeCast extends Function{
public TypeCast(DataType resultType){
super(resultType.name().toLowerCase(), resultType, false, DataType.PRIMITIVE);
}
@Override
public Object evaluate(Object... args){
return resultType.convert(args[0]);
}
}
public static class UserFunction extends Function{
public final XPathFunction xpathFunction;
public UserFunction(String namespace, String name, XPathFunction xpathFunction){
super(namespace, name, DataType.PRIMITIVE, true, DataType.PRIMITIVE);
this.xpathFunction = xpathFunction;
}
@Override
public Object evaluate(Object... args){
try{
return xpathFunction.evaluate(Arrays.asList(args));
}catch(XPathFunctionException ex){
throw new RuntimeException(ex);
}
}
}
/*-------------------------------------------------[ Arithmetic ]---------------------------------------------------*/
private static abstract class ArithmeticFunction extends PeekingFunction{
protected ArithmeticFunction(String name){
super(name, DataType.NUMBER, false, DataType.NUMBER, DataType.NUMBER);
}
@Override
protected final Object onMemberResult(int index, Object result){
Double d = (Double)result;
return d.isNaN() || d.isInfinite() ? d : null;
}
}
public static final ArithmeticFunction ADD = new ArithmeticFunction("+"){
@Override
public Object evaluate(Object... args){
return (Double)args[0] + (Double)args[1];
}
};
public static final ArithmeticFunction SUBSTRACT = new ArithmeticFunction("-"){
@Override
public Object evaluate(Object... args){
return (Double)args[0] - (Double)args[1];
}
};
public static final ArithmeticFunction MULTIPLY = new ArithmeticFunction("*"){
@Override
public Object evaluate(Object... args){
return (Double)args[0] * (Double)args[1];
}
};
public static final ArithmeticFunction DIV = new ArithmeticFunction("div"){
@Override
public Object evaluate(Object... args){
return (Double)args[0] / (Double)args[1];
}
};
public static final ArithmeticFunction MOD = new ArithmeticFunction("mod"){
@Override
public Object evaluate(Object... args){
return (Double)args[0] % (Double)args[1];
}
};
/*-------------------------------------------------[ Numeric ]---------------------------------------------------*/
public static final Function CEIL = new Function("ceiling", DataType.NUMBER, false, DataType.NUMBER, DataType.NUMBER){
@Override
public Object evaluate(Object... args){
Double d = (Double)args[0];
if(Double.isNaN(d) || Double.isInfinite(d))
return d;
return Math.ceil(d);
}
};
public static final Function FLOOR = new Function("floor", DataType.NUMBER, false, DataType.NUMBER, DataType.NUMBER){
@Override
public Object evaluate(Object... args){
Double d = (Double)args[0];
if(Double.isNaN(d) || Double.isInfinite(d))
return d;
return Math.floor(d);
}
};
public static final Function ROUND = new Function("round", DataType.NUMBER, false, DataType.NUMBER, DataType.NUMBER){
@Override
public Object evaluate(Object... args){
Double d = (Double)args[0];
if(Double.isNaN(d) || Double.isInfinite(d))
return d;
return (double)Math.round(d);
}
};
/*-------------------------------------------------[ String ]---------------------------------------------------*/
public static final Function LENGTH = new Function("string-length", DataType.NUMBER, false, DataType.STRING){
@Override
public Object evaluate(Object... args){
String str = (String)args[0];
return (double)str.codePointCount(0, str.length());
}
};
public static final Function STARTS_WITH = new Function("starts-with", DataType.BOOLEAN, false, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
return ((String)args[0]).startsWith((String)args[1]);
}
};
public static final Function ENDS_WITH = new Function("ends-with", DataType.BOOLEAN, false, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
return ((String)args[0]).endsWith((String)args[1]);
}
};
public static final Function CONTAINS = new Function("contains", DataType.BOOLEAN, false, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
return ((String)args[0]).contains((String)args[1]);
}
};
public static final Function CONCAT = new Function("concat", DataType.STRING, true, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
StringBuilder buff = new StringBuilder();
for(Object arg: args)
buff.append(arg);
return buff.toString();
}
};
public static final Function LANGUAGE_MATCH = new Function("language-match", DataType.BOOLEAN, false, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
String sublang = (String)args[0];
String lang = (String)args[1];
if(sublang.equalsIgnoreCase(lang))
return true;
int len = lang.length();
return
sublang.length() > len &&
sublang.charAt(len) == '-' &&
sublang.substring(0, len).equalsIgnoreCase(lang);
}
};
public static final Function TRANSLATE = new Function("translate", DataType.STRING, false, DataType.STRING, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
return translate((String)args[0], (String)args[1], (String)args[2]);
}
public String translate(String input, String from, String to){
// Initialize the mapping in a HashMap
Map<String, String> characterMap = new HashMap<String, String>();
String[] fromCharacters = toUnicodeCharacters(from);
String[] toCharacters = toUnicodeCharacters(to);
int fromLen = fromCharacters.length;
int toLen = toCharacters.length;
for(int i=0; i<fromLen; i++){
String cFrom = fromCharacters[i];
if(characterMap.containsKey(cFrom)) // We've seen the character before, ignore
continue;
if(i<toLen) // Will change
characterMap.put(cFrom, toCharacters[i]);
else // Will delete
characterMap.put(cFrom, null);
}
// Process the input string thru the map
StringBuilder output = new StringBuilder(input.length());
String[] inCharacters = toUnicodeCharacters(input);
int inLen = inCharacters.length;
for(int i=0; i<inLen; i++){
String cIn = inCharacters[i];
if(characterMap.containsKey(cIn)){
String cTo = characterMap.get(cIn);
if(cTo!=null)
output.append(cTo);
}else
output.append(cIn);
}
return output.toString();
}
private String[] toUnicodeCharacters(final String s){
String[] result = new String[s.length()];
int stringLength = 0;
int slen = s.length();
for(int i=0; i<slen; i++){
char c1 = s.charAt(i);
if(c1>=0xD800 && c1<=0xDBFF){ // isHighSurrogate(c1)
try{
char c2 = s.charAt(i+1);
if(c2>=0xDC00 && c2<=0xDFFF){ //isLowSurrogate(c2)
result[stringLength] = (c1 + "" + c2).intern();
i++;
}else
throw new IllegalArgumentException("Mismatched surrogate pair in translate function");
}catch (StringIndexOutOfBoundsException ex){
throw new IllegalArgumentException("High surrogate without low surrogate at end of string passed to translate function");
}
}else
result[stringLength] = String.valueOf(c1).intern();
stringLength++;
}
if(stringLength==result.length)
return result;
// trim array
String[] trimmed = new String[stringLength];
System.arraycopy(result, 0, trimmed, 0, stringLength);
return trimmed;
}
};
public static final Function NORMALIZE_SPACE = new Function("normalize-space", DataType.STRING, false, DataType.STRING){
@Override
public Object evaluate(Object... args){
return normalize((String)args[0]);
}
public String normalize(String str){
char[] buffer = str.toCharArray();
int write = 0;
int lastWrite = 0;
boolean wroteOne = false;
int read = 0;
while (read < buffer.length){
if (isXMLSpace(buffer[read])){
if (wroteOne)
buffer[write++] = ' ';
do{
read++;
}while(read < buffer.length && isXMLSpace(buffer[read]));
}else{
buffer[write++] = buffer[read++];
wroteOne = true;
lastWrite = write;
}
}
return new String(buffer, 0, lastWrite);
}
private boolean isXMLSpace(char c) {
return c==' ' || c=='\n' || c=='\r' || c=='\t';
}
};
public static final Function SUBSTRING = new Function("", "substring", DataType.STRING, false, 2, DataType.STRING, DataType.NUMBER, DataType.NUMBER){
@Override
public Object evaluate(Object... args){
String str = (String)args[0];
if(str==null)
return "";
int stringLength = ((Double)LENGTH.evaluate(str)).intValue();
if(stringLength==0)
return "";
Double d1 = (Double)args[1];
if(d1.isNaN())
return "";
int start = ((Double)ROUND.evaluate(d1)).intValue() - 1; // subtract 1 as Java strings are zero based
int substringLength = stringLength;
if(args.length==3){
Double d2 = (Double)args[2];
if(!d2.isNaN())
substringLength = ((Double)ROUND.evaluate(d2)).intValue();
else
substringLength = 0;
}
if (substringLength<0)
return "";
int end = start + substringLength;
if(args.length==2)
end = stringLength;
if(start<0) // negative start is treated as 0
start = 0;
else if(start>stringLength)
return "";
if(end>stringLength)
end = stringLength;
else if(end<start)
return "";
if(stringLength==str.length()) // // easy case; no surrogate pairs
return str.substring(start, end);
else
return unicodeSubstring(str, start, end);
}
private String unicodeSubstring(String s, int start, int end){
StringBuffer result = new StringBuffer(s.length());
for(int jChar=0, uChar=0; uChar<end; jChar++, uChar++){
char c = s.charAt(jChar);
if(uChar>=start)
result.append(c);
if(c>=0xD800){ // get the low surrogate
// ???? we could check here that this is indeed a low surroagte
// we could also catch StringIndexOutOfBoundsException
jChar++;
if(uChar>=start)
result.append(s.charAt(jChar));
}
}
return result.toString();
}
};
public static abstract class ChangeCase extends Function{
protected ChangeCase(String name){
super("", name, DataType.STRING, false, 1, DataType.STRING, DataType.STRING);
}
@Override
public Object evaluate(Object... args){
Locale locale = Locale.ENGLISH;
if(args.length>1){
locale = findLocale((String)args[1]);
if(locale==null)
locale = Locale.ENGLISH;
}
return evaluate((String)args[0], locale);
}
protected abstract String evaluate(String arg, Locale locale);
/**
* Tries to find a Locale instance by name using
* <a href="http://www.ietf.org/rfc/rfc3066.txt" target="_top">RFC 3066</a>
* language tags such as 'en', 'en-US', 'en-US-Brooklyn'.
*
* @param localeText the RFC 3066 language tag
* @return the locale for the given text or null if one could not
* be found
*/
public static Locale findLocale(String localeText) {
StringTokenizer tokens = new StringTokenizer( localeText, "-" );
if(tokens.hasMoreTokens()){
String language = tokens.nextToken();
if(!tokens.hasMoreTokens())
return findLocaleForLanguage(language);
else{
String country = tokens.nextToken();
if(!tokens.hasMoreTokens())
return new Locale(language, country);
else{
String variant = tokens.nextToken();
return new Locale(language, country, variant);
}
}
}
return null;
}
/**
* Finds the locale with the given language name with no country
* or variant, such as Locale.ENGLISH or Locale.FRENCH
*
* @param language the language code to look for
* @return the locale for the given language or null if one could not
* be found
*/
private static Locale findLocaleForLanguage(String language) {
for(Locale locale: Locale.getAvailableLocales()){
if(language.equals(locale.getLanguage())){
String country = locale.getCountry();
if(country==null || country.length()==0){
String variant = locale.getVariant();
if(variant==null || variant.length()==0)
return locale;
}
}
}
return null;
}
}
public static final ChangeCase UPPER_CASE = new ChangeCase("upper-case"){
@Override
protected String evaluate(String arg, Locale locale){
return arg.toUpperCase(locale);
}
};
public static final ChangeCase LOWER_CASE = new ChangeCase("lower-case"){
@Override
protected String evaluate(String arg, Locale locale){
return arg.toLowerCase(locale);
}
};
/*-------------------------------------------------[ Boolean ]---------------------------------------------------*/
public static final Function AND = new PeekingFunction("and", DataType.BOOLEAN, false, DataType.BOOLEAN, DataType.BOOLEAN){
@Override
public Object evaluate(Object... args){
return (Boolean)args[0] && (Boolean)args[1];
}
@Override
protected Object onMemberResult(int index, Object result){
return result==Boolean.FALSE ? result : null;
}
};
public static final Function OR = new PeekingFunction("or", DataType.BOOLEAN, false, DataType.BOOLEAN, DataType.BOOLEAN){
@Override
public Object evaluate(Object... args){
return (Boolean)args[0] || (Boolean)args[1];
}
@Override
protected Object onMemberResult(int index, Object result){
return result==Boolean.TRUE ? result : null;
}
};
public static final Function NOT = new Function("not", DataType.BOOLEAN, false, DataType.BOOLEAN){
@Override
public Object evaluate(Object... args){
return !(Boolean)args[0];
}
};
/*-------------------------------------------------[ Equals ]---------------------------------------------------*/
public static final Function NUMBER_EQUALS_NUMBER = new PeekingFunction("=", DataType.BOOLEAN, false, DataType.NUMBER, DataType.NUMBER){
@Override
public Object evaluate(Object... args){
return args[0].equals(args[1]);
}
@Override
protected Object onMemberResult(int index, Object result){
return Double.isNaN((Double)result) ? Boolean.FALSE : null;
}
};
public static final Function STRING_EQUALS_STRING = new Function("=", DataType.BOOLEAN, false, DataType.STRING, DataType.STRING){
@Override
public Object evaluate(Object... args){
assert args[0] instanceof String;
assert args[1] instanceof String;
return args[0].equals(args[1]);
}
};
public static final Function STRINGS_EQUALS_STRING = new PeekingFunction("=", DataType.BOOLEAN, false, DataType.STRINGS, DataType.STRING){
@Override
public Object evaluate(Object... args){
assert args[1] instanceof String;
if(args[0] instanceof Collection)
return ((Collection)args[0]).contains(args[1]);
else
return args[0].equals(args[1]);
}
@Override
protected Object onMemberResult(int index, Object result){
return result instanceof Collection && ((Collection)result).size()==0 ? Boolean.FALSE : null;
}
};
public static final Function NUMBERS_EQUALS_NUMBER = new PeekingFunction("=", DataType.BOOLEAN, false, DataType.NUMBERS, DataType.NUMBER){
@Override
public Object evaluate(Object... args){
double rhs = (Double)args[1];
if(Double.isNaN(rhs))
return false;
if(args[0] instanceof Collection)
return ((Collection)args[0]).contains(args[1]);
else
return (Double)args[0]==rhs;
}
@Override
protected Object onMemberResult(int index, Object result){
if(result instanceof Double)
return Double.isNaN((Double)result) ? Boolean.FALSE : null;
else
return ((Collection)result).size()==0 ? Boolean.FALSE : null;
}
};
public static final Function STRINGS_EQUALS_STRINGS = new PeekingFunction("=", DataType.BOOLEAN, false, DataType.STRINGS, DataType.STRINGS){
@Override
public Object evaluate(Object... args){
boolean list0 = args[0] instanceof Collection;
boolean list1 = args[1] instanceof Collection;
if(list0 && list1){
Collection rhs = (Collection)args[1];
for(Object lhs: (Collection)args[0]){
if(rhs.contains(lhs))
return true;
}
return false;
}else if(list0)
return ((Collection)args[0]).contains(args[1]);
else if(list1)
return ((Collection)args[1]).contains(args[0]);
else
return args[0].equals(args[1]);
}
@Override
protected Object onMemberResult(int index, Object result){
return result instanceof Collection && ((Collection)result).size()==0 ? Boolean.FALSE : null;
}
};
/*-------------------------------------------------[ Comparison ]---------------------------------------------------*/
private abstract static class Comparison extends PeekingFunction{
protected Comparison(String name, DataType memberType){
super(name, DataType.BOOLEAN, false, memberType, memberType);
}
@Override
public final Object evaluate(Object[] args){
boolean list1 = args[0] instanceof Collection;
boolean list2 = args[1] instanceof Collection;
if(list1 && list2){
Collection rhsCollection = (Collection)args[1];
for(Object lhs: (Collection)args[0]){
for(Object rhs: rhsCollection){
if(evaluateObjectObject(lhs, rhs))
return true;
}
}
}else if(list1){
for(Object lhs: (Collection)args[0]){
if(evaluateObjectObject(lhs, args[1]))
return true;
}
}else if(list2){
for(Object rhs: (Collection)args[1]){
if(evaluateObjectObject(args[0], rhs))
return true;
}
}else
return evaluateObjectObject(args[0], args[1]);
return false;
}
@Override
protected Object onMemberResult(int index, Object result){
return result instanceof Collection && ((Collection)result).size()==0 ? Boolean.FALSE : null;
}
protected abstract boolean evaluateObjectObject(Object lhs, Object rhs);
}
private static abstract class Equality extends Comparison{
public Equality(String name){
super(name, DataType.STRINGS);
}
@Override
protected final boolean evaluateObjectObject( Object lhs, Object rhs){
assert lhs!=null && rhs!=null;
if(lhs instanceof Boolean || rhs instanceof Boolean)
return evaluateObjects(DataType.asBoolean(lhs), DataType.asBoolean(rhs));
else if(lhs instanceof Double || rhs instanceof Double)
return evaluateObjects(DataType.asNumber(lhs), DataType.asNumber(rhs));
else
return evaluateObjects(lhs.toString(), rhs.toString());
}
protected abstract boolean evaluateObjects(Object lhs, Object rhs);
}
public static final Function EQUALS = new Equality("="){
@Override
protected boolean evaluateObjects(Object lhs, Object rhs){
if(lhs instanceof Double){
if(Double.isNaN((Double)lhs) || Double.isNaN((Double)rhs))
return false;
}
return lhs.equals(rhs);
}
@Override
protected final Object onMemberResult(int index, Object result){
if(result instanceof Double)
return Double.isNaN((Double)result) ? Boolean.FALSE : null;
return super.onMemberResult(index, result);
}
};
public static final Function NOT_EQUALS = new Equality("!="){
@Override
protected boolean evaluateObjects(Object lhs, Object rhs){
if(lhs instanceof Double){
if(Double.isNaN((Double)lhs) || Double.isNaN((Double)rhs))
return true;
}
return !lhs.equals(rhs);
}
@Override
protected final Object onMemberResult(int index, Object result){
if(result instanceof Double)
return Double.isNaN((Double)result) ? Boolean.TRUE : null;
return super.onMemberResult(index, result);
}
};
/*-------------------------------------------------[ Relational ]---------------------------------------------------*/
private static abstract class Relational extends PeekingFunction{
public Relational(String name){
super(name, DataType.BOOLEAN, false, DataType.NUMBERS, DataType.NUMBERS);
}
public final Object evaluate(Object[] args){
boolean list1 = args[0] instanceof Collection;
boolean list2 = args[1] instanceof Collection;
if(list1 && list2){
Collection rhsCollection = (Collection)args[1];
for(Object lhs: (Collection)args[0]){
double lhsNum = (Double)lhs;
if(!Double.isNaN(lhsNum)){
for(Object rhs: rhsCollection){
double rhsNum = (Double)rhs;
if(!Double.isNaN(rhsNum) && evaluateDoubles(lhsNum, rhsNum))
return true;
}
}
}
}else if(list1){
double rhsNum = (Double)args[1];
if(!Double.isNaN(rhsNum)){
for(Object lhs: (Collection)args[0]){
double lhsNum = (Double)lhs;
if(!Double.isNaN(lhsNum) && evaluateDoubles(lhsNum, rhsNum))
return true;
}
}
}else if(list2){
double lhsNum = (Double)args[0];
if(!Double.isNaN(lhsNum)){
for(Object rhs: (Collection)args[1]){
double rhsNum = (Double)rhs;
if(!Double.isNaN(rhsNum) && evaluateDoubles(lhsNum, rhsNum))
return true;
}
}
}else{
double lhsNum = (Double)args[0];
if(Double.isNaN(lhsNum))
return false;
double rhsNum = (Double)args[1];
return !Double.isNaN(rhsNum) && evaluateDoubles(lhsNum, rhsNum);
}
return false;
}
protected abstract boolean evaluateDoubles(double lhs, double rhs);
@Override
protected final Object onMemberResult(int index, Object result){
if(result instanceof Collection)
return ((Collection)result).size()==0 ? Boolean.FALSE : null;
else
return ((Double)result).isNaN() ? Boolean.FALSE : null;
}
}
public static final Function GREATER_THAN = new Relational(">"){
@Override
protected boolean evaluateDoubles(double lhs, double rhs){
return lhs>rhs;
}
};
public static final Function GREATER_THAN_EQUAL = new Relational(">="){
@Override
protected boolean evaluateDoubles(double lhs, double rhs){
return lhs>=rhs;
}
};
public static final Function LESS_THAN = new Relational("<"){
@Override
protected boolean evaluateDoubles(double lhs, double rhs){
return lhs<rhs;
}
};
public static final Function LESS_THAN_EQUAL = new Relational("<="){
@Override
protected boolean evaluateDoubles(double lhs, double rhs){
return lhs<=rhs;
}
};
/*-------------------------------------------------[ Lookup ]---------------------------------------------------*/
public static final Map<String, Function> library = new HashMap<String, Function>();
static{
Function functions[] = {
ADD, SUBSTRACT, MULTIPLY, DIV, MOD,
CEIL, FLOOR, ROUND,
LENGTH, STARTS_WITH, ENDS_WITH, CONTAINS, CONCAT, LANGUAGE_MATCH, TRANSLATE, NORMALIZE_SPACE, SUBSTRING, UPPER_CASE, LOWER_CASE,
AND, OR, NOT,
EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_EQUAL, LESS_THAN, LESS_THAN_EQUAL,
new TypeCast(DataType.STRING), new TypeCast(DataType.NUMBER), new TypeCast(DataType.BOOLEAN)
};
for(Function f: functions)
library.put(f.name, f);
}
}