/* 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 <http://www.gnu.org/licenses/>. */ package org.voltdb; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; 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()); } /** * Given the results of a procedure, convert it into a sensible array of VoltTables. * @throws InvocationTargetException */ final static public VoltTable[] getResultsFromRawResults(String procedureName, Object result) throws InvocationTargetException { if (result == null) { return new VoltTable[0]; } if (result instanceof VoltTable[]) { VoltTable[] retval = (VoltTable[]) result; for (VoltTable table : retval) { if (table == null) { Exception e = new RuntimeException("VoltTable arrays with non-zero length cannot contain null values."); throw new InvocationTargetException(e); } // Make sure this table does not use an ee cache buffer table.convertToHeapBuffer(); } return retval; } if (result instanceof VoltTable) { VoltTable vt = (VoltTable) result; // Make sure this table does not use an ee cache buffer vt.convertToHeapBuffer(); return new VoltTable[] { vt }; } if (result instanceof Long) { VoltTable t = new VoltTable(new VoltTable.ColumnInfo("", VoltType.BIGINT)); t.addRow(result); return new VoltTable[] { t }; } throw new RuntimeException(String.format("Procedure %s unsupported procedure return type %s.", procedureName, result.getClass().getSimpleName())); } }