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

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

The newest version!
/*
 * Firebird Open Source 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 source control history command.
 *
 * All rights reserved.
 */
package org.firebirdsql.jdbc.escape;

import org.firebirdsql.util.InternalApi;
import org.jspecify.annotations.Nullable;

import java.util.*;

/**
 * Helper class for escaped functions.
 *
 * @author Roman Rokytskyy
 * @author Mark Rotteveel
 */
@InternalApi
public final 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 {
        final Map functionMap = new HashMap<>(71);
        /* Numeric Functions */
        Map numericFunctionMap = getNumericFunctions();
        SUPPORTED_NUMERIC_FUNCTIONS = Set.copyOf(numericFunctionMap.keySet());
        functionMap.putAll(numericFunctionMap);

        /* String Functions */
        Map stringFunctionMap = getStringFunctions();
        SUPPORTED_STRING_FUNCTIONS = Set.copyOf(stringFunctionMap.keySet());
        functionMap.putAll(stringFunctionMap);

        /* Time and Date Functions */
        Map timeDateFunctionMap = getTimeDateFunctions();
        SUPPORTED_TIME_DATE_FUNCTIONS = Set.copyOf(timeDateFunctionMap.keySet());
        functionMap.putAll(timeDateFunctionMap);

        /* System Functions */
        Map systemFunctionMap = getSystemFunctions();
        SUPPORTED_SYSTEM_FUNCTIONS = Set.copyOf(systemFunctionMap.keySet());
        functionMap.putAll(systemFunctionMap);

        /* Conversion Functions */
        functionMap.put("CONVERT", new ConvertFunction());

        // Unsupported functions defined in appendix D that might accidentally work due to UDFs
        // Numerics
        functionMap.put("RAND", null);

        // String
        functionMap.put("DIFFERENCE", null);
        functionMap.put("SOUNDEX", null);

        // Time and date
        // ...none

        // System
        // ...none

        FUNCTION_MAP = Collections.unmodifiableMap(functionMap);
    }

    private FBEscapedFunctionHelper() {
        // no instances
    }

    private static Map getNumericFunctions() {
        Map functionMap = new HashMap<>(32);
        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("DEGREES", new PatternSQLFunction("(({0})*180.0/PI())"));
        functionMap.put("EXP", null);
        functionMap.put("FLOOR", null);
        functionMap.put("LOG", new PatternSQLFunction("LN({0})"));
        functionMap.put("LOG10", null);
        functionMap.put("MOD", null);
        functionMap.put("PI", null);
        functionMap.put("POWER", null);
        functionMap.put("RADIANS", new PatternSQLFunction("(({0})*PI()/180.0)"));
        functionMap.put("ROUND", null);
        functionMap.put("SIGN", null);
        functionMap.put("SIN", null);
        functionMap.put("SQRT", null);
        functionMap.put("TAN", null);
        functionMap.put("TRUNCATE", new PatternSQLFunction("TRUNC({0},{1})"));

        return functionMap;
    }

    private static Map getStringFunctions() {
        Map functionMap = new HashMap<>(32);
        functionMap.put("ASCII", new PatternSQLFunction("ASCII_VAL({0})"));
        functionMap.put("CHAR", new PatternSQLFunction("ASCII_CHAR({0})"));
        CharacterLengthFunction characterLengthFunction = new CharacterLengthFunction();
        functionMap.put("CHAR_LENGTH", characterLengthFunction);
        functionMap.put("CHARACTER_LENGTH", characterLengthFunction);
        functionMap.put("CONCAT", new PatternSQLFunction("({0}||{1})"));
        functionMap.put("INSERT", new PatternSQLFunction("OVERLAY({0} PLACING {3} FROM {1} FOR {2})"));
        functionMap.put("LCASE", new PatternSQLFunction("LOWER({0})"));
        functionMap.put("LEFT", null);
        functionMap.put("LENGTH", new LengthFunction());
        functionMap.put("LOCATE", new LocateFunction());
        functionMap.put("LTRIM", new PatternSQLFunction("TRIM(LEADING FROM {0})"));
        functionMap.put("OCTET_LENGTH", null);
        // NOTE We're only supporting CHARACTERS optional parameter (OCTETS unclear or technically not possible)
        functionMap.put("POSITION", new PositionFunction());
        // Doubling of single quotes due to MessageFormat requirements
        functionMap.put("REPEAT", new PatternSQLFunction("RPAD('''',{1},{0})"));
        functionMap.put("REPLACE", null);
        functionMap.put("RIGHT", null);
        functionMap.put("RTRIM", new PatternSQLFunction("TRIM(TRAILING FROM {0})"));
        // Doubling of single quotes due to MessageFormat requirements
        functionMap.put("SPACE", new PatternSQLFunction("RPAD('''',{0})"));
        // NOTE We're not discerning between optional CHARACTERS / OCTETS parameter (technically not possible?)
        functionMap.put("SUBSTRING", new PatternSQLFunction("SUBSTRING({0} FROM {1} FOR {2})"));
        functionMap.put("UCASE", new PatternSQLFunction("UPPER({0})"));

        return functionMap;
    }

    private static Map getTimeDateFunctions() {
        Map functionMap = new HashMap<>(32);
        ConstantSQLFunction currentDate = new ConstantSQLFunction("CURRENT_DATE");
        functionMap.put("CURRENT_DATE", currentDate);
        ConstantSQLFunction currentTime = new ConstantSQLFunction("CURRENT_TIME");
        functionMap.put("CURRENT_TIME", currentTime);
        ConstantSQLFunction currentTimestamp = new ConstantSQLFunction("CURRENT_TIMESTAMP");
        functionMap.put("CURRENT_TIMESTAMP", currentTimestamp);
        functionMap.put("CURDATE", currentDate);
        functionMap.put("CURTIME", currentTime);
        functionMap.put("DAYNAME", new PatternSQLFunction("trim(decode(extract(weekday from {0}), "
                + "0, ''Sunday'', "
                + "1, ''Monday'', "
                + "2, ''Tuesday'', "
                + "3, ''Wednesday'', "
                + "4, ''Thursday'', "
                + "5, ''Friday'', "
                + "6, ''Saturday''))"));
        functionMap.put("DAYOFMONTH", new PatternSQLFunction("EXTRACT(DAY FROM {0})"));
        functionMap.put("DAYOFWEEK", new PatternSQLFunction("EXTRACT(WEEKDAY FROM {0})+1"));
        functionMap.put("DAYOFYEAR", new PatternSQLFunction("EXTRACT(YEARDAY FROM {0})+1"));
        functionMap.put("EXTRACT", null);
        functionMap.put("HOUR", new PatternSQLFunction("EXTRACT(HOUR FROM {0})"));
        functionMap.put("MINUTE", new PatternSQLFunction("EXTRACT(MINUTE FROM {0})"));
        functionMap.put("MONTH", new PatternSQLFunction("EXTRACT(MONTH FROM {0})"));
        functionMap.put("MONTHNAME", new PatternSQLFunction("trim(decode(extract(month from {0}), "
                + "1, ''January'', "
                + "2, ''February'', "
                + "3, ''March'', "
                + "4, ''April'', "
                + "5, ''May'', "
                + "6, ''June'', "
                + "7, ''July'', "
                + "8, ''August'', "
                + "9, ''September'', "
                + "10, ''October'', "
                + "11, ''November'', "
                + "12, ''December''))"));
        functionMap.put("NOW", currentTimestamp);
        functionMap.put("QUARTER", new PatternSQLFunction("(1+(EXTRACT(MONTH FROM {0})-1)/3)"));
        functionMap.put("SECOND", new PatternSQLFunction("EXTRACT(SECOND FROM {0})"));
        functionMap.put("TIMESTAMPADD", new TimestampAddFunction());
        functionMap.put("TIMESTAMPDIFF", new TimestampDiffFunction());
        functionMap.put("WEEK", new PatternSQLFunction("EXTRACT(WEEK FROM {0})"));
        functionMap.put("YEAR", new PatternSQLFunction("EXTRACT(YEAR FROM {0})"));
        
        return functionMap;
    }

    private static Map getSystemFunctions() {
        Map functionMap = new HashMap<>(4, 1.0f);
        functionMap.put("DATABASE", new ConstantSQLFunction("RDB$GET_CONTEXT('SYSTEM', 'DB_NAME')"));
        functionMap.put("IFNULL", new PatternSQLFunction("COALESCE({0}, {1})"));
        functionMap.put("USER", new ConstantSQLFunction("USER"));

        return 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 var params = new ArrayList();
        final var 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;
            }
            case '"' -> {
                sb.append(currentChar);
                if (!inQuotes)
                    inDoubleQuotes = !inDoubleQuotes;
                coalesceSpace = false;
            }
            case '(' -> {
                if (!(inQuotes || inDoubleQuotes)) {
                    nestedParentheses++;
                }
                sb.append('(');
                coalesceSpace = false;
            }
            case ')' -> {
                if (!(inQuotes || inDoubleQuotes)) {
                    nestedParentheses--;
                    if (nestedParentheses < 0) {
                        throw new FBSQLParseException("Unbalanced parentheses in parameters at position " + i);
                    }
                }
                sb.append(')');
                coalesceSpace = false;
            }
            // comma is a 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;
                }
            }
            // by default, we add chars to the buffer
            default -> {
                sb.append(currentChar);
                coalesceSpace = false;
            }
            }
        }

        // add last parameter if present
        if (!sb.isEmpty()) {
            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 @Nullable String convertTemplate(final String functionCall) throws FBSQLParseException {
        final String functionName = parseFunction(functionCall).toUpperCase(Locale.ROOT);
        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."
             */
            throw new FBSQLParseException("Unsupported JDBC function escape: " + functionName);
        }

        final SQLFunction firebirdTemplate = FUNCTION_MAP.get(functionName);

        if (firebirdTemplate != null) {
            return firebirdTemplate.apply(params);
        }

        return null;
    }

    /**
     * @return Set of JDBC numeric functions supported (as defined in appendix C.1 of JDBC 4.3)
     */
    public static Set getSupportedNumericFunctions() {
        return SUPPORTED_NUMERIC_FUNCTIONS;
    }

    /**
     * @return Set of JDBC string functions supported (as defined in appendix C.2 of JDBC 4.3)
     */
    public static Set getSupportedStringFunctions() {
        return SUPPORTED_STRING_FUNCTIONS;
    }

    /**
     * @return Set of JDBC time and date functions supported (as defined in appendix C.3 of JDBC 4.3)
     */
    public static Set getSupportedTimeDateFunctions() {
        return SUPPORTED_TIME_DATE_FUNCTIONS;
    }

    /**
     * @return Set of JDBC system functions supported (as defined in appendix C.4 of JDBC 4.3)
     */
    public static Set getSupportedSystemFunctions() {
        return SUPPORTED_SYSTEM_FUNCTIONS;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy