view src/main/java/nl/cwi/monetdb/embedded/EmbeddedPreparedStatement.java @ 40:8a65996a8dc0 embedded

FIxed conversions and closing statements
author Pedro Ferreira <pedro.ferreira@monetdbsolutions.com>
date Fri, 04 Nov 2016 17:51:33 +0100 (2016-11-04)
parents 068ec5964f28
children
line wrap: on
line source
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2016 MonetDB B.V.
 */

package nl.cwi.monetdb.embedded;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.InetAddress;
import java.net.URI;
import java.sql.PreparedStatement;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * The embedded version of the {@link PreparedStatement} interface from JDBC (not inheriting for easier implementation).
 *
 * @author <a href="mailto:pedro.ferreira@monetdbsolutions.com">Pedro Ferreira</a>, Fabian Groffen, Martin van Dinther
 */
public class EmbeddedPreparedStatement {

    /**
     * A formatter for Time columns without timezone.
     */
    private static final SimpleDateFormat TimeFormatter;

    /**
     * A formatter for Time columns with timezone.
     */
    private static final SimpleDateFormat TimeTzFormatter;

    /**
     * A formatter for Timestamp columns without timezone.
     */
    private static final SimpleDateFormat TimestampFormatter;

    /**
     * A formatter for Timestamp columns with timezone.
     */
    private static final SimpleDateFormat TimestampTzFormatter;

    /**
     * A list of Java classes that don't need special parsing of values (jsut call toString() method).
     */
    private static final Set<Class<?>> DirectMappingClasses;

    static {
        TimeFormatter = new SimpleDateFormat("HH:mm:ss.SSS");
        TimeTzFormatter = new SimpleDateFormat("HH:mm:ss.SSSZ");
        TimestampFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        TimestampTzFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ");
        DirectMappingClasses = new HashSet<>();
        DirectMappingClasses.add(Boolean.class);
        DirectMappingClasses.add(Byte.class);
        DirectMappingClasses.add(Short.class);
        DirectMappingClasses.add(Integer.class);
        DirectMappingClasses.add(Long.class);
        DirectMappingClasses.add(BigInteger.class);
        DirectMappingClasses.add(Float.class);
        DirectMappingClasses.add(Double.class);
        DirectMappingClasses.add(MonetDBEmbeddedBlob.class);
        DirectMappingClasses.add(URI.class);
        DirectMappingClasses.add(InetAddress.class);
        DirectMappingClasses.add(UUID.class);
    }

    /**
     * The corresponding connection for this Prepared statement.
     */
    private final MonetDBEmbeddedConnection connection;

    /**
     * The id of the generated prepare statement.
     */
    private final int prepareId;

    /**
     * The number of parsed parameters (?) in the statement.
     */
    private final int numberOfParameters;

    /**
     * The list of MonetDB-to-Java mappings of the parameters.
     */
    private final MonetDBToJavaMapping[] parametersMappings;

    /**
     * The number of digits allowed for each parameter.
     */
    private final int[] parametersDigits;

    /**
     * The scale for each parameter.
     */
    private final int[] parametersScales;

    /**
     * The schema name corresponding for each parameter (or null if not corresponding).
     */
    private final String[] parametersSchemas;

    /**
     * The table name corresponding for each parameter (or null if not corresponding).
     */
    private final String[] parametersTables;

    /**
     * The column name corresponding for each parameter (or null if not corresponding).
     */
    private final String[] parametersNames;

    /**
     * The parsed value for each parameter (or null if not parsed).
     */
    private final String[] parsedValues;

    protected EmbeddedPreparedStatement(MonetDBEmbeddedConnection connection, int prepareId,
                                        MonetDBToJavaMapping[] parametersMappings, int[] parametersDigits,
                                        int[] parametersScales, String[] parametersSchemas, String[] parametersTables,
                                        String[] parametersNames, String[] parsedValues) throws MonetDBEmbeddedException {
        this.connection = connection;
        this.prepareId = prepareId;
        this.numberOfParameters = parametersMappings.length;
        this.parametersMappings = parametersMappings;
        this.parametersDigits = parametersDigits;
        this.parametersScales = parametersScales;
        this.parametersSchemas = parametersSchemas;
        this.parametersTables = parametersTables;
        this.parametersNames = parametersNames;
        this.parsedValues = parsedValues;
    }

    /**
     * Get the corresponding connection for this statement.
     *
     * @return A MonetDBEmbeddedConnection instance
     */
    public MonetDBEmbeddedConnection getConnection() {
        return connection;
    }

    /**
     * Get the number of parsed parameters (?) in the statement.
     *
     * @return The number of parsed parameters (?) in the statement
     */
    public int getNumberOfParameters() { return numberOfParameters; }

    /**
     * Gets the list of MonetDB-to-Java mappings of the parameters.
     *
     * @return The list of MonetDB-to-Java mappings of the parameters
     */
    public MonetDBToJavaMapping[] getParametersMappings() {
        return parametersMappings;
    }

    /**
     * Gets the number of digits allowed for each parameter.
     *
     * @return The number of digits allowed for each parameter
     */
    public int[] getParametersDigits() {
        return parametersDigits;
    }

    /**
     * Gets the scale for each parameter.
     *
     * @return The scale for each parameter
     */
    public int[] getParametersScales() {
        return parametersScales;
    }

    /**
     * Gets the schema name corresponding for each parameter (or null if not corresponding).
     *
     * @return The schema name corresponding for each parameter (or null if not corresponding)
     */
    public String[] getParametersSchemas() {
        return parametersSchemas;
    }

    /**
     * Gets the table name corresponding for each parameter (or null if not corresponding).
     *
     * @return The table name corresponding for each parameter (or null if not corresponding)
     */
    public String[] getParametersTables() {
        return parametersTables;
    }

    /**
     * Gets the column name corresponding for each parameter (or null if not corresponding).
     *
     * @return The column name corresponding for each parameter (or null if not corresponding)
     */
    public String[] getParametersNames() {
        return parametersNames;
    }

    /**
     * Gets the parsed value for each parameter (or null if not parsed).
     *
     * @return The parsed value for each parameter (or null if not parsed)
     */
    public String[] getParsedValues() {
        return parsedValues;
    }

    /**
     * Clears the current parsed values.
     */
    public void clearParameters() {
        for (int i = 0; i < parsedValues.length; i++) {
            parsedValues[i] = null;
        }
    }

    /**
     * Converts a Java String into the MonetDB representation according to its type.
     *
     * @param parameter The index of the parameter
     * @param value A String value to parse
     * @return The parsed String as a String
     * @throws MonetDBEmbeddedException If the type is a char or varchar an the length is longer than allowed
     */
    private String setString(int parameter, String value) throws MonetDBEmbeddedException {
        String type = this.parametersMappings[parameter].toString();
        if((type.equals("Char") || type.equals("Varchar")) && value.length() > this.parametersDigits[parameter]) {
            throw new MonetDBEmbeddedException("The length is higher than allowed: " + value.length() + " > "
                    + this.parametersDigits[parameter] + "!");
        } else {
            return value;
        }
    }

    /**
     * Converts a Java BigDecimal into a MonetDB decimal representation (adapted from the JDBC driver implementation).
     *
     * @param parameter The index of the parameter
     * @param value A BigDecimal value to parse
     * @return The parsed BigDecimal as a String
     * @throws MonetDBEmbeddedException If the value exceeds the allowed digits or scale
     */
    private String setBigDecimal(int parameter, BigDecimal value) throws MonetDBEmbeddedException {
        // round to the scale of the DB:
        value = value.setScale(this.parametersScales[parameter], RoundingMode.HALF_UP);
        if (value.precision() > this.parametersDigits[parameter]) {
            throw new MonetDBEmbeddedException("DECIMAL value exceeds allowed digits/scale: " + value.toPlainString()
                    + " (" + this.parametersDigits[parameter] + "/" + this.parametersScales[parameter] + ")");
        }

        // MonetDB doesn't like leading 0's, since it counts them as part of
        // the precision, so let's strip them off. (But be careful not to do
        // this to the exact number "0".)  Also strip off trailing
        // numbers that are inherent to the double representation.
        String xStr = value.toPlainString();
        int dot = xStr.indexOf('.');
        if (dot >= 0)
            xStr = xStr.substring(0, Math.min(xStr.length(), dot + 1 + this.parametersScales[parameter]));
        while (xStr.startsWith("0") && xStr.length() > 1)
            xStr = xStr.substring(1);
        return xStr;
    }

    /**
     * Converts a Java SQL Time into a MonetDB time representation (adapted from the JDBC driver implementation).
     *
     * @param parameter The index of the parameter
     * @param value A Java SQL Time value to parse
     * @return The parsed Java SQL Time as a String
     */
    private String setTime(int parameter, Time value) {
        boolean hasTimeZone = this.parametersMappings[parameter].toString().endsWith("Tz");
        if(hasTimeZone) {
            String RFC822 = TimeTzFormatter.format(value);
            return RFC822.substring(0, 15) + ":" + RFC822.substring(15);
        } else {
            return TimeFormatter.format(value);
        }
    }

    /**
     * Converts a Java SQL Timestamp into a MonetDB time representation (adapted from the JDBC driver implementation).
     *
     * @param parameter The index of the parameter
     * @param value A Java SQL Timestamp value to parse
     * @return The parsed Java SQL Timestamp as a String
     */
    private String setTimestamp(int parameter, Timestamp value) {
        boolean hasTimeZone = this.parametersMappings[parameter].toString().endsWith("Tz");
        if(hasTimeZone) {
            String RFC822 = TimestampTzFormatter.format(value);
            return RFC822.substring(0, 26) + ":" + RFC822.substring(26);
        } else {
            return TimestampFormatter.format(value);
        }
    }

    /**
     * Converts a Java Class into the corresponding MonetDB representation in a parameter.
     *
     * @param <T> The Java Class to map to MonetDB
     * @param parameter The index of the parameter
     * @param value The instance of the Java class to map
     * @throws MonetDBEmbeddedException If the Java class has no mapping in a MonetDB datatype
     */
    public <T> void setParameterValue(int parameter, T value) throws MonetDBEmbeddedException {
        this.setParameterValue(parameter, value, this.parametersMappings[parameter].getJavaClass());
    }

    /**
     * Converts a Java Class into the corresponding MonetDB representation in a parameter.
     *
     * @param <T> The Java Class to map to MonetDB
     * @param parameter The index of the parameter
     * @param value The instance of the Java class to map
     * @param javaClass The class of the instance
     * @throws MonetDBEmbeddedException If the Java class has no mapping in a MonetDB datatype
     */
    public <T> void setParameterValue(int parameter, T value, Class<T> javaClass) throws MonetDBEmbeddedException {
        if(value == null) {
            this.setParameterNull(parameter);
        } else {
            String valueToSubmit;
            if(DirectMappingClasses.contains(javaClass)) {
                valueToSubmit = value.toString();
            } else if(javaClass.equals(String.class)) {
                valueToSubmit = this.setString(parameter, (String) value);
            } else if(javaClass.equals(BigDecimal.class)) {
                valueToSubmit = this.setBigDecimal(parameter, (BigDecimal) value);
            } else if(javaClass.equals(Time.class)) {
                valueToSubmit = this.setTime(parameter, (Time) value);
            } else if(javaClass.equals(Timestamp.class)) {
                valueToSubmit = this.setTimestamp(parameter, (Timestamp) value);
            } else {
                throw new MonetDBEmbeddedException("The class " + javaClass.getSimpleName() +
                        " is not supported by the mapping!");
            }
            this.parsedValues[parameter] = "'" + valueToSubmit.replaceAll("\\\\", "\\\\\\\\")
                    .replaceAll("'", "\\\\'") + "'";
        }
    }

    /**
     * Set a parameter as a null value.
     *
     * @param parameter The index of the parameter
     */
    public void setParameterNull(int parameter) {
        this.parsedValues[parameter] = "NULL";
    }

    /**
     * Creates the SQL String from the parsed parameters (adapted from the JDBC driver implementation).
     *
     * @throws MonetDBEmbeddedException If a parameter has not been set yet
     */
    private String applyParameters() throws MonetDBEmbeddedException {
        StringBuilder buf = new StringBuilder(8 + 12 * this.numberOfParameters).append("exec ")
                .append(this.prepareId).append('(');
        int col = 0;
        for (int i = 0; i < this.numberOfParameters; i++) {
            if (this.parametersNames[i] != null)
                continue;
            col++;
            if (col > 1)
                buf.append(',');
            if (this.parsedValues[i] == null) {
                throw new MonetDBEmbeddedException("Cannot execute, parameter " + col + " is missing!");
            }
            buf.append(this.parsedValues[i]);
        }
        buf.append(')');
        return buf.toString();
    }

    /**
     * Executes this statement as a SQL query without a result set.
     *
     * @return The update result object
     * @throws MonetDBEmbeddedException If an error in the database occurred or if a parameter has not been set yet
     */
    public UpdateResultSet sendUpdate() throws MonetDBEmbeddedException {
        return this.connection.sendUpdate(this.applyParameters());
    }

    /**
     * Executes this statement as a SQL query with a result set.
     *
     * @return The query result object
     * @throws MonetDBEmbeddedException If an error in the database occurred or if a parameter has not been set yet
     */
    public QueryResultSet sendQuery() throws MonetDBEmbeddedException {
        return this.connection.sendQuery(this.applyParameters());
    }

    /**
     * Executes this statement as a SQL query without a result set asynchronously.
     *
     * @return The update result object
     * @throws MonetDBEmbeddedException If an error in the database occurred or if a parameter has not been set yet
     */
    public UpdateResultSet sendUpdateAsync() throws MonetDBEmbeddedException {
        return this.connection.sendUpdateAsync(this.applyParameters());
    }

    /**
     * Executes this statement as a SQL query with a result set asynchronously.
     *
     * @return The query result object
     * @throws MonetDBEmbeddedException If an error in the database occurred or if a parameter has not been set yet
     */
    public QueryResultSet sendQueryAsync() throws MonetDBEmbeddedException {
        return this.connection.sendQueryAsync(this.applyParameters());
    }
}