/** * 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); } }