All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.firebirdsql.jdbc.escape.FBEscapedFunctionHelper Maven / Gradle / Ivy

There is a newer version: 4.0.10.java7
Show newest version
/*
 * Firebird Open Source J2ee connector - jdbc driver
 *
 * Distributable under LGPL license.
 * You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * LGPL License for more details.
 *
 * This file was created by members of the firebird development team.
 * All individual contributions remain the Copyright (C) of those
 * individuals.  Contributors to this file are either listed here or
 * can be obtained from a CVS history command.
 *
 * All rights reserved.
 */
package org.firebirdsql.jdbc.escape;

import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.*;

import org.firebirdsql.jdbc.escape.FBEscapedParser.EscapeParserMode;

/**
 * Helper class for escaped functions.
 * 
 * @author Roman Rokytskyy
 */
public class FBEscapedFunctionHelper {
    
    /**
     * This map contains mapping between JDBC function names and Firebird ones.
     * Mapping to null means: execute as is (might fail if there is no built-in or UDF)
     */
    private static final Map FUNCTION_MAP;
    
    /**
     * Supported numeric functions
     */
    private static final Set SUPPORTED_NUMERIC_FUNCTIONS;
    
    /**
     * Supported string functions
     */
    private static final Set SUPPORTED_STRING_FUNCTIONS;
    
    /**
     * Supported time and date functions
     */
    private static final Set SUPPORTED_TIME_DATE_FUNCTIONS;
    
    /**
     * Supported system functions
     */
    private static final Set SUPPORTED_SYSTEM_FUNCTIONS;
    
    static {
        Map functionMap = new HashMap();
        /* Numeric Functions */
        functionMap.put("ABS", null);
        functionMap.put("ACOS", null);
        functionMap.put("ASIN", null);
        functionMap.put("ATAN", null);
        functionMap.put("ATAN2", null);
        functionMap.put("CEILING", null);
        functionMap.put("COS", null);
        functionMap.put("COT", null);
        functionMap.put("EXP", null);
        functionMap.put("FLOOR", null);
        functionMap.put("LOG", "LN({0})");
        functionMap.put("LOG10", null);
        functionMap.put("MOD", null);
        functionMap.put("PI", null);
        functionMap.put("POWER", null);
        functionMap.put("ROUND", null);
        functionMap.put("SIGN", null);
        functionMap.put("SIN", null);
        functionMap.put("SQRT", null);
        functionMap.put("TAN", null);
        functionMap.put("TRUNCATE", "TRUNC({0},{1})");
        
        SUPPORTED_NUMERIC_FUNCTIONS = Collections.unmodifiableSet(new HashSet(functionMap.keySet()));
        
        /* String Functions */
        functionMap.put("ASCII", "ASCII_VAL({0})");
        functionMap.put("CHAR", "ASCII_CHAR({0})");
        // TODO support difference between CHARACTER and OCTETS optional param
        functionMap.put("CHAR_LENGTH", "CHAR_LENGTH({0})");
        functionMap.put("CHARACTER_LENGTH", "CHAR_LENGTH({0})");
        functionMap.put("CONCAT", "{0}||{1}");
        functionMap.put("INSERT", "OVERLAY({0} PLACING {3} FROM {1} FOR {2})");
        functionMap.put("LCASE", "LOWER({0})");
        functionMap.put("LEFT", null);
        // TODO support difference between CHARACTER and OCTETS optional param
        functionMap.put("LENGTH", "CHAR_LENGTH(TRIM(TRAILING FROM {0}))");
        // TODO Support variant without start position (required for JavaEE compliance see 6.2 of JDBC 4.1 spec
        functionMap.put("LOCATE", "POSITION({0},{1},{2})");
        functionMap.put("LTRIM", "TRIM(LEADING FROM {0})");
        functionMap.put("OCTET_LENGTH", null);
        functionMap.put("POSITION", null);
        functionMap.put("REPEAT", "RPAD('''',{1},{0})");
        functionMap.put("REPLACE", null);
        functionMap.put("RIGHT", null);
        functionMap.put("RTRIM", "TRIM(TRAILING FROM {0})");
        functionMap.put("SPACE", "RPAD('''',{0})");
        functionMap.put("SUBSTRING", "SUBSTRING({0} FROM {1} FOR {2})");
        functionMap.put("UCASE", "UPPER({0})");
        
        Set supportedStringFunctions = new HashSet(functionMap.keySet());
        supportedStringFunctions.removeAll(SUPPORTED_NUMERIC_FUNCTIONS);
        SUPPORTED_STRING_FUNCTIONS = Collections.unmodifiableSet(new HashSet(supportedStringFunctions));
        
        /* Time and Date Functions */
        functionMap.put("CURRENT_DATE", "CURRENT_DATE");
        functionMap.put("CURRENT_TIME", "CURRENT_TIME");
        functionMap.put("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP");
        functionMap.put("CURDATE", "CURRENT_DATE");
        functionMap.put("CURTIME", "CURRENT_TIME");
        functionMap.put("DAYOFMONTH", "EXTRACT(DAY FROM {0})");
        functionMap.put("DAYOFWEEK", "EXTRACT(WEEKDAY FROM {0})+1");
        functionMap.put("DAYOFYEAR", "EXTRACT(YEARDAY FROM {0})+1");
        functionMap.put("EXTRACT", null);
        functionMap.put("HOUR", "EXTRACT(HOUR FROM {0})");
        functionMap.put("MINUTE", "EXTRACT(MINUTE FROM {0})");
        functionMap.put("MONTH", "EXTRACT(MONTH FROM {0})");
        functionMap.put("NOW", "CURRENT_TIMESTAMP");
        functionMap.put("SECOND", "EXTRACT(SECOND FROM {0})");
        functionMap.put("TIMESTAMPADD", null);
        functionMap.put("TIMESTAMPDIFF", null);
        functionMap.put("WEEK", "EXTRACT(WEEK FROM {0})");
        functionMap.put("YEAR", "EXTRACT(YEAR FROM {0})");
        
        Set supportedTimeDateFunctions = new HashSet(functionMap.keySet());
        supportedTimeDateFunctions.removeAll(SUPPORTED_NUMERIC_FUNCTIONS);
        supportedTimeDateFunctions.removeAll(SUPPORTED_STRING_FUNCTIONS);
        SUPPORTED_TIME_DATE_FUNCTIONS = Collections.unmodifiableSet(new HashSet(supportedTimeDateFunctions));
        
        /* System Functions */
        functionMap.put("IFNULL", "COALESCE({0}, {1})");
        functionMap.put("USER", "USER");
        
        Set supportedSystemFunctions = new HashSet(functionMap.keySet());
        supportedSystemFunctions.removeAll(SUPPORTED_NUMERIC_FUNCTIONS);
        supportedSystemFunctions.removeAll(SUPPORTED_STRING_FUNCTIONS);
        supportedSystemFunctions.removeAll(SUPPORTED_TIME_DATE_FUNCTIONS);
        SUPPORTED_SYSTEM_FUNCTIONS = Collections.unmodifiableSet(new HashSet(supportedSystemFunctions));
        
        /* Conversion Functions */
        // TODO This does not support the conversion with SQL_ prefix in appendix D
        // TODO Should work without specifying size on CHAR and VARCHAR
        functionMap.put("CONVERT", "CAST({0} AS {1})");
        
        // Unsupported functions defined in appendix D that might accidentally work due to UDFs
        // Numerics
        functionMap.put("DEGREES", null);
        functionMap.put("RADIANS", null);
        functionMap.put("RAND", null);
        
        // String
        functionMap.put("DIFFERENCE", null);
        functionMap.put("SOUNDEX", null);
        
        // Time and date
        functionMap.put("DAYNAME", null);
        functionMap.put("MONTHNAME", null);
        functionMap.put("QUARTER", null);
        
        // System
        functionMap.put("DATABASE", null);
        
        FUNCTION_MAP = Collections.unmodifiableMap(functionMap);
    }
    
    /**
     * Simple syntax check if function is specified in form "name(...)".
     * 
     * @param functionCall string representing function call.
     * 
     * @throws FBSQLParseException if simple syntax check failed.
     */
    private static void checkSyntax(String functionCall) throws FBSQLParseException {
        // NOTE: Some function calls don't require parenthesis eg CURRENT_TIMESTAMP
        int parenthesisStart = functionCall.indexOf('(');
        if (parenthesisStart != -1 && functionCall.charAt(functionCall.length() - 1) != ')')
            throw new FBSQLParseException("No closing parenthesis found, not a function call.");
    }
    
    /**
     * Extract function name from the function call.
     * 
     * @param functionCall escaped function call.
     * 
     * @return name of the function.
     * 
     * @throws FBSQLParseException if parse error occurs.
     */
    public static String parseFunction(String functionCall) throws FBSQLParseException {
        functionCall = functionCall.trim();
        checkSyntax(functionCall);
        int parenthesisStart = functionCall.indexOf('(');
        
        return parenthesisStart != -1 ? functionCall.substring(0, parenthesisStart) : functionCall;
    }
    
    /**
     * Extract function arguments from the function call. This method parses 
     * escaped function call string and extracts function parameters from it.
     * 
     * @param functionCall escaped function call.
     * 
     * @return list of parameters of the function.
     * 
     * @throws FBSQLParseException if parse error occurs.
     */
    public static List parseArguments(String functionCall) throws FBSQLParseException {
        functionCall = functionCall.trim();
        checkSyntax(functionCall);

        final int parenthesisStart = functionCall.indexOf('(');
        if (parenthesisStart == -1) {
            return Collections.emptyList();
        }
        final String paramsString = functionCall.substring(parenthesisStart + 1, functionCall.length() - 1);

        final List params = new ArrayList();
        final StringBuilder sb = new StringBuilder();
        boolean inQuotes = false;
        boolean inDoubleQuotes = false;
        // ignore initial whitespace
        boolean coalesceSpace = true;
        int nestedParentheses = 0;

        for (int i = 0, n = paramsString.length(); i < n; i++) {
            char currentChar = paramsString.charAt(i);
            // we coalesce spaces, tabs and new lines into a single space if
            // we are not in a string literal
            if (Character.isWhitespace(currentChar)) {
                if (inQuotes || inDoubleQuotes) {
                    sb.append(currentChar);
                } else if (!coalesceSpace) {
                    sb.append(' ');
                    coalesceSpace = true;
                }
                continue;
            }
            switch (currentChar) {
            case '\'':
                sb.append(currentChar);
                if (!inDoubleQuotes)
                	inQuotes = !inQuotes;
                coalesceSpace = false;
                break;
            case '"':
                sb.append(currentChar);
                if (!inQuotes)
                	inDoubleQuotes = !inDoubleQuotes;
                coalesceSpace = false;
                break;
            case '(':
                if (!(inQuotes || inDoubleQuotes)) {
                    nestedParentheses++;
                }
                sb.append('(');
                coalesceSpace = false;
                break;
            case ')':
                if (!(inQuotes || inDoubleQuotes)) {
                    nestedParentheses--;
                    if (nestedParentheses < 0) {
                        throw new FBSQLParseException("Unbalanced parentheses in parameters at position " + i);
                    }
                }
                sb.append(')');
                coalesceSpace = false;
                break;
            // comma is considered parameter separator
            // if it is not within the string literal or within parentheses 
            case ',':
                if (inQuotes || inDoubleQuotes || nestedParentheses > 0) {
                    sb.append(currentChar);
                } else {
                    params.add(sb.toString());
                    sb.setLength(0);
                    // Ignore whitespace after parameter
                    coalesceSpace = true;
                }
                break;
            // by default we add chars to the buffer  
            default:
                sb.append(currentChar);
                coalesceSpace = false;
            }
        }

        // add last parameter if present
        if (sb.length() > 0)
        	params.add(sb.toString());

        // after processing all parameters all string literals should be closed
        if (inQuotes || inDoubleQuotes) {
            throw new FBSQLParseException("String literal is not properly closed.");
        }
        if (nestedParentheses != 0) {
            throw new FBSQLParseException("Unbalanced parentheses in parameters.");
        }

        return params;
    }
    
    /**
     * Convert escaped function call using function template.
     * 
     * @param functionCall escaped function call.
     * 
     * @return server-side representation of the function call or null
     * if no template found.
     *  
     * @throws FBSQLParseException if escaped function call has incorrect syntax.
     */
    public static String convertTemplate(final String functionCall, final EscapeParserMode mode) throws FBSQLParseException {
        final String functionName = parseFunction(functionCall).toUpperCase();
        final String[] params = parseArguments(functionCall).toArray(new String[0]);
        
        if (!FUNCTION_MAP.containsKey(functionName)) {
            /* See 13.4.1 of JDBC 4.1 spec:
             * "The escape syntax for scalar functions must only be used to invoke the scalar
             * functions defined in Appendix D "Scalar Functions". The escape syntax is not
             * intended to be used to invoke user-defined or vendor specific scalar functions."
             */
            // TODO Consider throwing SQLFeatureNotSupported or a different SQLException
            throw new FBSQLParseException("Unsupported JDBC function escape: " + functionName);
        }
        
        final String firebirdTemplate = FUNCTION_MAP.get(functionName);

        if (firebirdTemplate != null) 
            return MessageFormat.format(firebirdTemplate, (Object[]) params);
        
        if (mode == EscapeParserMode.USE_STANDARD_UDF)
            return convertUsingStandardUDF(functionName, params);
            
        return null;
    }
    
    /*
     * Functions below are conversion routines of the escaped function calls
     * into the standard UDF library functions. The conversion function must
     * have the name equal to the function name in the escaped syntax in the
     * lower case and should take array of strings as parameter and it may throw
     * the FBSQLParseException and must be declared as static and have public
     * visibility. It should return a string of the converted function call.
     */
    
    private static String convertUsingStandardUDF(String name, String[] params) throws FBSQLParseException {
        try {
            name = name.toLowerCase();
            Method method = FBEscapedFunctionHelper.class.getMethod(name, new Class[] { String[].class});
            return (String)method.invoke(null, new Object[]{params});
        } catch(NoSuchMethodException ex) {
            return null;
        } catch (Exception ex) {
            throw new FBSQLParseException("Error when converting function " 
                + name + ". Error " + ex.getClass().getName() + 
                " : " + ex.getMessage());
        }
    }
    
    /*
     * Mathematical functions
     */
    
    /**
     * Produce a function call for the abs UDF function. 
     * The syntax of the abs function is 
     * {fn abs(number)}.
     * 
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String abs(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function abs : " + params.length);
        
        return "abs(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the acos UDF function. 
     * The syntax of the acos function is 
     * {fn acos(float)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String acos(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function acos : " + params.length);
        
        return "acos(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the asin UDF function. 
     * The syntax of the asin function is 
     * {fn asin(float)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String asin(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function asin : " + params.length);
        
        return "asin(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the atan UDF function. 
     * The syntax of the atan function is 
     * {fn atan(float)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String atan(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function atan : " + params.length);
        
        return "atan(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the atan2 UDF function. 
     * The syntax of the atan2 function is 
     * {fn atan2(float1, float2)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String atan2(String[] params) throws FBSQLParseException {
        if (params.length != 2)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function atan2 : " + params.length);
        
        return "atan2(" + params[0] + ", " + params[1] + ")";
    }
    
    /**
     * Produce a function call for the ceiling UDF function. 
     * The syntax of the ceiling function is 
     * {fn ceiling(number)}.
     *  
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String ceiling(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function ceiling : " + params.length);
        
        return "ceiling(" + params[0] + ")";
    }

    /**
     * Produce a function call for the cos UDF function. 
     * The syntax of the cos function is 
     * {fn cos(float)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String cos(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function cos : " + params.length);
        
        return "cos(" + params[0] + ")";
    }

    /**
     * Produce a function call for the cot UDF function. 
     * The syntax of the cot function is 
     * {fn cot(float)}.
     * 
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String cot(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function cot : " + params.length);
        
        return "cot(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the floor UDF function. 
     * The syntax of the floor function is 
     * {fn floor(number)}.
     * 
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String floor(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function floor : " + params.length);
        
        return "floor(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the log10 UDF function. 
     * The syntax of the log10 function is 
     * {fn log10(number)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String log10(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function log10 : " + params.length);
        
        return "log10(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the mod UDF function. 
     * The syntax of the mod function is 
     * {fn mod(integer1, integer2)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String mod(String[] params) throws FBSQLParseException {
        if (params.length != 2)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function mod : " + params.length);
        
        return "mod(" + params[0] + ", " + params[1] + ")";
    }
    
    /**
     * Produce a function call for the pi UDF function. 
     * The syntax of the pi function is {fn pi()}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String pi(String[] params) throws FBSQLParseException {
        if (params.length != 0)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function pi : " + params.length);
        
        return "pi()";
    }
    
    /**
     * Produce a function call for the rand UDF function. 
     * The syntax for the rand function is 
     * {fn rand()}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String rand(String[] params) throws FBSQLParseException {
        if (params.length != 0)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function rand : " + params.length);
        
        return "rand()";
    }
    
    /**
     * Produce a function call for the sign UDF function. 
     * The syntax for the sign function is 
     * {fn sign(number)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String sign(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function sign : " + params.length);
        
        return "sign(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the sin UDF function.
     * The syntax for the sin function is 
     * {fn sin(float)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String sin(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function sin : " + params.length);
        
        return "sin(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the sqrt UDF function.
     * The syntax for the sqrt function is
     * {fn sqrt(number)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String sqrt(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function sqrt : " + params.length);
        
        return "sqrt(" + params[0] + ")";
    }
    
    /**
     * Produce a function call for the tan UDF function.
     * The syntax for the tan function is
     * {fn tan(float)}.
     *
     * @param params The parameters to be used in the call
     * @throws FBSQLParseException if there is an error with the parameters
     */
    public static String tan(String[] params) throws FBSQLParseException {
        if (params.length != 1)
            throw new FBSQLParseException("Incorrect number of " +
                    "parameters of function tan : " + params.length);
        
        return "tan(" + params[0] + ")";
    }
    
    /**
     * @return Set of JDBC numeric functions supported (as defined in appendix D.1 of JDBC 4.1)
     */
    public static Set getSupportedNumericFunctions() {
        return SUPPORTED_NUMERIC_FUNCTIONS;
    }
    
    /**
     * @return Set of JDBC string functions supported (as defined in appendix D.2 of JDBC 4.1)
     */
    public static Set getSupportedStringFunctions() {
        return SUPPORTED_STRING_FUNCTIONS;
    }
    
    /**
     * @return Set of JDBC time and date functions supported (as defined in appendix D.3 of JDBC 4.1)
     */
    public static Set getSupportedTimeDateFunctions() {
        return SUPPORTED_TIME_DATE_FUNCTIONS;
    }
    
    /**
     * @return Set of JDBC system functions supported (as defined in appendix D.4 of JDBC 4.1)
     */
    public static Set getSupportedSystemFunctions() {
        return SUPPORTED_SYSTEM_FUNCTIONS;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy