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

org.voltdb.ParameterConverter Maven / Gradle / Ivy

There is a newer version: 13.3.2-preview1
Show newest version
/* This file is part of VoltDB.
 * Copyright (C) 2008-2017 VoltDB Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * 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
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with VoltDB.  If not, see .
 */

package org.voltdb;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.regex.Pattern;

import org.voltdb.common.Constants;
import org.voltdb.parser.SQLParser;
import org.voltdb.types.GeographyPointValue;
import org.voltdb.types.GeographyValue;
import org.voltdb.types.TimestampType;
import org.voltdb.types.VoltDecimalHelper;
import org.voltdb.utils.Encoder;

/**
 * ParameterConverter provides a static helper to convert a deserialized
 * procedure invocation parameter to the correct Object required by a
 * stored procedure's parameter type.
 *
 */
public class ParameterConverter {

    /**
     * Get the appropriate and compatible null value for a given
     * parameter type.
     */
    private static Object nullValueForType(final Class expectedClz)
    {
        if (expectedClz == long.class) {
            return VoltType.NULL_BIGINT;
        }
        else if (expectedClz == int.class) {
            return VoltType.NULL_INTEGER;
        }
        else if (expectedClz == short.class) {
            return VoltType.NULL_SMALLINT;
        }
        else if (expectedClz == byte.class) {
            return VoltType.NULL_TINYINT;
        }
        else if (expectedClz == double.class) {
            return VoltType.NULL_FLOAT;
        }

        // all non-primitive types can handle null
        return null;
    }

    /**
     * Assertion-heavy method to verify the type tryToMakeCompatible returns
     * is exactly the type asked for (or null in some cases).
     */
    public static boolean verifyParameterConversion(
            Object value,
            final Class expectedClz)
    {
        if (expectedClz == long.class) {
            assert(value != null);
            assert(value.getClass() == Long.class);
        }
        else if (expectedClz == int.class) {
            assert(value != null);
            assert(value.getClass() == Integer.class);
        }
        else if (expectedClz == short.class) {
            assert(value != null);
            assert(value.getClass() == Short.class);
        }
        else if (expectedClz == byte.class) {
            assert(value != null);
            assert(value.getClass() == Byte.class);
        }
        else if (expectedClz == double.class) {
            assert(value != null);
            assert(value.getClass() == Double.class);
        }
        else if (value != null) {
            Class clz = value.getClass();
            if (clz != expectedClz) {
                // skip this without linking to it (used for sysprocs)
                return expectedClz.getSimpleName().equals("SystemProcedureExecutionContext") &&
                        expectedClz.isAssignableFrom(clz);
            }
            if (expectedClz.isArray()) {
                assert(clz.getComponentType() == expectedClz.getComponentType());
            }
        }
        return true;
    }

    private static final Pattern thousandSeparator = Pattern.compile("\\,");

    /**
     * Given a string, covert it to a primitive type or return null.
     *
     * If the string value is a VARBINARY constant of the form X'00ABCD', and the
     * expected class is one of byte, short, int or long, then we interpret the
     * string as specifying bits of a 64-bit signed integer (padded with zeroes if
     * there are fewer than 16 digits).
     * Corresponding code for handling hex literals appears in HSQL's ExpressionValue class
     * and in voltdb.expressions.ConstantValueExpression.
     */
    private static Object convertStringToPrimitive(String value, final Class expectedClz)
    throws VoltTypeException
    {
        value = value.trim();
        // detect CSV null
        if (value.equals(Constants.CSV_NULL)) return nullValueForType(expectedClz);

        // Remove commas.  Doing this seems kind of dubious since it lets strings like
        //    ,,,3.1,4,,e,+,,16
        // be parsed as a valid double value (for example).
        String commaFreeValue = thousandSeparator.matcher(value).replaceAll("");

        try {
            if (expectedClz == long.class) {
                return Long.parseLong(commaFreeValue);
            }
            if (expectedClz == int.class) {
                return Integer.parseInt(commaFreeValue);
            }
            if (expectedClz == short.class) {
                return Short.parseShort(commaFreeValue);
            }
            if (expectedClz == byte.class) {
                return Byte.parseByte(commaFreeValue);
            }
            if (expectedClz == double.class) {
                return Double.parseDouble(commaFreeValue);
            }
        }
        // ignore the exception and fail through below
        catch (NumberFormatException nfe) {

            // If we failed to parse the string in decimal form it could still
            // be a numeric value specified as X'....'
            //
            // Do this only after trying to parse a decimal literal, which is the
            // most common case.
            if (expectedClz != double.class) {
                String hexDigits = SQLParser.getDigitsFromHexLiteral(value);
                if (hexDigits != null) {
                    try {
                        return SQLParser.hexDigitsToLong(hexDigits);
                    }
                    catch (SQLParser.Exception spe) {
                    }
                }
            }
        }

        throw new VoltTypeException(
                "tryToMakeCompatible: Unable to convert string "
                + value + " to "  + expectedClz.getName()
                + " value for target parameter.");
    }

    /**
     * Factored out code to handle array parameter types.
     *
     * @throws Exception with a message describing why the types are incompatible.
     */
    private static Object tryToMakeCompatibleArray(
            final Class expectedComponentClz,
            final Class inputComponentClz,
            Object param)
    throws VoltTypeException
    {
        int inputLength = Array.getLength(param);

        if (inputComponentClz == expectedComponentClz) {
            return param;
        }
        // if it's an empty array, let it through
        // this is a bit ugly as it might hide passing
        //  arrays of the wrong type, but it "does the right thing"
        //  more often that not I guess...
        else if (inputLength == 0) {
            return Array.newInstance(expectedComponentClz, 0);
        }
        // hack to make strings work with input as bytes
        else if ((inputComponentClz == byte[].class) && (expectedComponentClz == String.class)) {
            String[] values = new String[inputLength];
            for (int i = 0; i < inputLength; i++) {
                try {
                    values[i] = new String((byte[]) Array.get(param, i), "UTF-8");
                } catch (UnsupportedEncodingException ex) {
                    throw new VoltTypeException(
                            "tryScalarMakeCompatible: Unsupported encoding:"
                            + expectedComponentClz.getName() + " to provided " + inputComponentClz.getName());
                }
            }
            return values;
        }
        // hack to make varbinary work with input as hex string
        else if ((inputComponentClz == String.class) && (expectedComponentClz == byte[].class)) {
            byte[][] values = new byte[inputLength][];
            for (int i = 0; i < inputLength; i++) {
                values[i] = Encoder.hexDecode((String) Array.get(param, i));
            }
            return values;
        }
        else {
            /*
             * Arrays can be quite large so it doesn't make sense to silently do the conversion
             * and incur the performance hit. The client should serialize the correct invocation
             * parameters
             */
            throw new VoltTypeException(
                    "tryScalarMakeCompatible: Unable to match parameter array:"
                    + expectedComponentClz.getName() + " to provided " + inputComponentClz.getName());
        }
    }

    /**
     * Convert the given value to the type given, if possible.
     *
     * This function is in the performance path, so some effort has been made to order
     * the giant string of branches such that most likely things are first, and that
     * if the type is already correct, it should move very quickly through the logic.
     * Some clarity has been sacrificed for performance, but perfect clarity is pretty
     * elusive with complicated logic like this anyway.
     *
     * @throws Exception with a message describing why the types are incompatible.
     */
    public static Object tryToMakeCompatible(final Class expectedClz, final Object param)
    throws VoltTypeException
    {
        /* uncomment for debugging
        System.err.printf("Converting %s of type %s to type %s\n",

                String.valueOf(param),
                param == null ? "NULL" : param.getClass().getName(),
                expectedClz.getName());
        System.err.flush();
        // */

        // Get blatant null out of the way fast, as it avoids some inline checks
        // There are some subtle null values that aren't java null coming up, but wait until
        // after the basics to check for those.
        if (param == null) {
            return nullValueForType(expectedClz);
        }

        Class inputClz = param.getClass();

        // If we make it through this first block, memoize a number value for some range checks later
        Number numberParam = null;

        // This first code block tries to hit as many common cases as possible
        // Specifically, it does primitive types and strings, which are the most common param types.
        // Downconversions (e.g. long => short) happen later, but can use the memoized numberParam value.
        // Notice this block switches on the type of the given value (different later).

        if (inputClz == Long.class) {
            if (expectedClz == long.class) return param;
            if ((Long) param == VoltType.NULL_BIGINT) return nullValueForType(expectedClz);
            numberParam = (Number) param;
        }
        else if (inputClz == Integer.class) {
            if (expectedClz == int.class) return param;
            if ((Integer) param == VoltType.NULL_INTEGER) return nullValueForType(expectedClz);
            if (expectedClz == long.class) return ((Integer) param).longValue();
            numberParam = (Number) param;
        }
        else if (inputClz == Short.class) {
            if (expectedClz == short.class) return param;
            if ((Short) param == VoltType.NULL_SMALLINT) return nullValueForType(expectedClz);
            if (expectedClz == long.class) return ((Short) param).longValue();
            if (expectedClz == int.class) return ((Short) param).intValue();
            numberParam = (Number) param;
        }
        else if (inputClz == Byte.class) {
            if (expectedClz == byte.class) return param;
            if ((Byte) param == VoltType.NULL_TINYINT) return nullValueForType(expectedClz);
            if (expectedClz == long.class) return ((Byte) param).longValue();
            if (expectedClz == int.class) return ((Byte) param).intValue();
            if (expectedClz == short.class) return ((Byte) param).shortValue();
            numberParam = (Number) param;
        }
        else if (inputClz == Double.class) {
            if (expectedClz == double.class) return param;
            if ((Double) param == VoltType.NULL_FLOAT) return nullValueForType(expectedClz);
        }
        else if (inputClz == String.class) {
            String stringParam = (String)param;
            if (stringParam.equals(Constants.CSV_NULL)) return nullValueForType(expectedClz);
            else if (expectedClz == String.class) return param;
            // Hack allows hex-encoded strings to be passed into byte[] params
            else if (expectedClz == byte[].class) {
                // regular expressions can be expensive, so don't invoke SQLParser
                // unless the param really looks like an x-quoted literal
                if (stringParam.startsWith("X") || stringParam.startsWith("x")) {
                    String hexDigits = SQLParser.getDigitsFromHexLiteral(stringParam);
                    if (hexDigits != null) {
                        stringParam = hexDigits;
                    }
                }
                return Encoder.hexDecode(stringParam);
            }
            // We allow all values to be passed as strings for csv loading, json, etc...
            // This code handles primitive types. Complex types come later.
            if (expectedClz.isPrimitive()) {
                return convertStringToPrimitive(stringParam, expectedClz);
            }
        }
        else if (inputClz == byte[].class) {
            if (expectedClz == byte[].class) return param;
            // allow byte arrays to be passed into string parameters
            else if (expectedClz == String.class) {
                String value = new String((byte[]) param, Constants.UTF8ENCODING);
                if (value.equals(Constants.CSV_NULL)) return nullValueForType(expectedClz);
                else return value;
            }
        }
        // null sigils. (ning - if we're not checking if the sigil matches the expected type,
        // why do we have three sigils for three types??)
        else if (param == VoltType.NULL_TIMESTAMP ||
                param == VoltType.NULL_STRING_OR_VARBINARY ||
                param == VoltType.NULL_GEOGRAPHY ||
                param == VoltType.NULL_POINT ||
                param == VoltType.NULL_DECIMAL) {
            return nullValueForType(expectedClz);
        }

        // make sure we get the array/scalar match
        if (expectedClz.isArray() != inputClz.isArray()) {
            throw new VoltTypeException(String.format("Array / Scalar parameter mismatch (%s to %s)",
                    inputClz.getName(), expectedClz.getName()));
        }

        // handle arrays in a factored-out method
        if (expectedClz.isArray()) {
            return tryToMakeCompatibleArray(expectedClz.getComponentType(), inputClz.getComponentType(), param);
        }

        // The following block switches on the type of the parameter desired.
        // It handles all of the paths not trapped in the code above. We can assume
        // values are not null and that most sane primitive stuff has been handled.
        // Downcasting is handled here (e.g. long => short).
        // Time (in many forms) and Decimal are also handled below.

        if ((expectedClz == int.class) && (numberParam != null)) {
            long val = numberParam.longValue();
            if (val == VoltType.NULL_INTEGER) {
                throw new VoltTypeException("tryToMakeCompatible: The provided long value: ("
                        + param.toString() + ") might be interpreted as integer null. " +
                                "Try explicitly using a int parameter.");
            }
            // if it's in the right range, crop the value and return
            if ((val <= Integer.MAX_VALUE) && (val >= Integer.MIN_VALUE))
                return numberParam.intValue();
        }
        else if ((expectedClz == short.class) && (numberParam != null)) {
            if ((inputClz == Long.class) || (inputClz == Integer.class)) {
                long val = numberParam.longValue();
                if (val == VoltType.NULL_SMALLINT) {
                    throw new VoltTypeException("tryToMakeCompatible: The provided int or long value: ("
                            + param.toString() + ") might be interpreted as smallint null. " +
                                    "Try explicitly using a short parameter.");
                }
                // if it's in the right range, crop the value and return
                if ((val <= Short.MAX_VALUE) && (val >= Short.MIN_VALUE))
                    return numberParam.shortValue();
            }
        }
        else if ((expectedClz == byte.class) && (numberParam != null)) {
            if ((inputClz == Long.class) || (inputClz == Integer.class) || (inputClz == Short.class)) {
                long val = numberParam.longValue();
                if (val == VoltType.NULL_TINYINT) {
                    throw new VoltTypeException("tryToMakeCompatible: The provided short, int or long value: ("
                            + param.toString() + ") might be interpreted as tinyint null. " +
                                    "Try explicitly using a byte parameter.");
                }
                // if it's in the right range, crop the value and return
                if ((val <= Byte.MAX_VALUE) && (val >= Byte.MIN_VALUE))
                    return numberParam.byteValue();
            }
        }
        else if ((expectedClz == double.class) && (numberParam != null)) {
            return numberParam.doubleValue();
        }
        else if (expectedClz == TimestampType.class) {
            if (inputClz == Integer.class) return new TimestampType((Integer)param); // null values safe
            if (inputClz == Long.class) return new TimestampType((Long)param); // null values safe
            if (inputClz == TimestampType.class) return param;
            if (inputClz == Date.class) return new TimestampType((Date) param);
            // if a string is given for a date, use java's JDBC parsing
            if (inputClz == String.class) {
                String timestring = ((String) param).trim();
                try {
                    return new TimestampType(Long.parseLong(timestring));
                } catch (IllegalArgumentException e) {
                    // Defer errors to the generic Exception throw below, if it's not the right format
                }
                try {
                    return SQLParser.parseDate(timestring);
                }
                catch (IllegalArgumentException e) {
                    // Defer errors to the generic Exception throw below, if it's not the right format
                }
            }
        }
        else if (expectedClz == java.sql.Timestamp.class) {
            if (param instanceof java.sql.Timestamp) return param;
            if (param instanceof java.util.Date) return new java.sql.Timestamp(((java.util.Date) param).getTime());
            if (param instanceof TimestampType) return ((TimestampType) param).asJavaTimestamp();
            // If a string is given for a date, use java's JDBC parsing.
            if (inputClz == String.class) {
                String longtime = ((String) param).trim();
                try {
                    return new java.sql.Timestamp(Long.parseLong(longtime));
                }
                catch (IllegalArgumentException e) {
                    // Defer errors to the generic Exception throw below, if it's not the right format
                }
                try {
                    return java.sql.Timestamp.valueOf(longtime);
                } catch (IllegalArgumentException e) {
                    // Defer errors to the generic Exception throw below, if it's not the right format
                }

            }
        }
        else if (expectedClz == java.sql.Date.class) {
            if (param instanceof java.sql.Date) return param; // covers java.sql.Date and java.sql.Timestamp
            if (param instanceof java.util.Date) return new java.sql.Date(((java.util.Date) param).getTime());
            if (param instanceof TimestampType) return ((TimestampType) param).asExactJavaSqlDate();
            // If a string is given for a date, use java's JDBC parsing.
            if (inputClz == String.class) {
                try {
                    return new java.sql.Date(TimestampType.millisFromJDBCformat((String) param));
                }
                catch (IllegalArgumentException e) {
                    // Defer errors to the generic Exception throw below, if it's not the right format
                }
            }
        }
        else if (expectedClz == java.util.Date.class) {
            if (param instanceof java.util.Date) return param; // covers java.sql.Date and java.sql.Timestamp
            if (param instanceof TimestampType) return ((TimestampType) param).asExactJavaDate();
            // If a string is given for a date, use the default format parser for the default locale.
            if (inputClz == String.class) {
                try {
                    return new java.util.Date(TimestampType.millisFromJDBCformat((String) param));
                }
                catch (IllegalArgumentException e) {
                    // Defer errors to the generic Exception throw below, if it's not the right format
                }
            }
        }
        else if (expectedClz == BigDecimal.class) {
            if (numberParam != null) {
                BigDecimal bd = VoltDecimalHelper.stringToDecimal(param.toString());
                return bd;
            }
            if (inputClz == BigDecimal.class) {
                BigDecimal bd = (BigDecimal) param;
                bd = VoltDecimalHelper.setDefaultScale(bd);
                return bd;
            }
            if (inputClz == Float.class || inputClz == Double.class) {
                try {
                    return VoltDecimalHelper.deserializeBigDecimalFromString(String.format("%.12f", param));
                } catch (IOException ex) {
                    throw new VoltTypeException(String.format("deserialize Float from string failed. (%s to %s)",
                            inputClz.getName(), expectedClz.getName()));
                }
            }
            try {
                return VoltDecimalHelper.deserializeBigDecimalFromString(String.valueOf(param));
            } catch (IOException ex) {
                throw new VoltTypeException(String.format("deserialize BigDecimal from string failed. (%s to %s)",
                        inputClz.getName(), expectedClz.getName()));
            }
        } else if (expectedClz == GeographyPointValue.class) {
            // Is it a point already?  If so, just return it.
            if (inputClz == GeographyPointValue.class) {
                return param;
            }
            // Is it a string from which we can construct a point?
            // If so, return the newly constructed point.
            if (inputClz == String.class) {
                try {
                    GeographyPointValue pt = GeographyPointValue.fromWKT((String)param);
                    return pt;
                } catch (IllegalArgumentException e) {
                    throw new VoltTypeException(String.format("deserialize GeographyPointValue from string failed (string %s)",
                                                              (String)param));
                }
            }
        } else if (expectedClz == GeographyValue.class) {
            if (inputClz == GeographyValue.class) {
                return param;
            }
            if (inputClz == String.class) {
                String paramStr = (String)param;
                try {
                    GeographyValue gv = GeographyValue.fromWKT(paramStr);
                    return gv;
                } catch (IllegalArgumentException e) {
                    throw new VoltTypeException(String.format("deserialize GeographyValue from string failed (string %s)",
                                                              paramStr));
                }
            }
        } else if (expectedClz == VoltTable.class && inputClz == VoltTable.class) {
            return param;
        } else if (expectedClz == String.class) {
            //For VARCHAR columns if not null or not an array send toString value.
            if (!param.getClass().isArray()) {
                return String.valueOf(param);
            }
        }

        // handle SystemProcedureExecutionContext without linking to it
        // these are used by system procedures and are ignored here
        if (expectedClz.getSimpleName().equals("SystemProcedureExecutionContext")) {
            if (expectedClz.isAssignableFrom(inputClz)) {
                return param;
            }
        }

        throw new VoltTypeException(
                "tryToMakeCompatible: The provided value: (" + param.toString() + ") of type: " + inputClz.getName() +
                " is not a match or is out of range for the target parameter type: " + expectedClz.getName());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy