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

org.firebirdsql.jdbc.AbstractGeneratedKeysQuery Maven / Gradle / Ivy

There is a newer version: 2.2.7
Show newest version
/*
 * $Id: AbstractGeneratedKeysQuery.java 56937 2012-04-04 18:31:34Z mrotteveel $
 * 
 * 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;

import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.firebirdsql.jdbc.parser.JaybirdStatementModel;
import org.firebirdsql.jdbc.parser.StatementParser;
import org.firebirdsql.jdbc.parser.StatementParser.ParseException;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;

/**
 * Class to add the RETURNING clause to queries for returning generated keys.
 * 
 * @author Mark Rotteveel
 * @since 2.2
 */
public abstract class AbstractGeneratedKeysQuery {

    private static final int QUERY_TYPE_KEEP_UNMODIFIED = 1;
    private static final int QUERY_TYPE_ADD_ALL_COLUMNS = 2;
    private static final int QUERY_TYPE_ADD_INDEXED = 3;
    private static final int QUERY_TYPE_ADD_COLUMNS = 4;
    private static final int QUERY_TYPE_ALREADY_HAS_RETURNING = 5;
    
    private static final StatementParser parser;
    static {
    	// Attempt to load statement parser
        StatementParser temp = null;
        try {
            temp = (StatementParser)Class.forName("org.firebirdsql.jdbc.parser.StatementParserImpl").newInstance();
        } catch (Throwable ex) {
            // Unable to load class of parser implementation, antlr-runtime not in path
            Logger log = LoggerFactory.getLogger(AbstractGeneratedKeysQuery.class, true);
            if (log != null) {
                log.error("Unable to load generated key parser", ex);
            }
        } finally {
            parser = temp;
        }
    }

    private final String originalSQL;
    private String modifiedSQL;
    private int queryType = QUERY_TYPE_KEEP_UNMODIFIED;
    private int[] columnIndexes;
    private String[] columnNames;
    private boolean processed = false;
    private boolean generatesKeys = false;
    private JaybirdStatementModel statementModel;

    private AbstractGeneratedKeysQuery(String sql) {
        originalSQL = sql;
    }

    /**
     * Process SQL statement text according to autoGeneratedKeys value.
     * 

* For Statement.NO_GENERATED_KEYS the statement will not be processed, for * Statement.RETURN_GENERATED_KEYS it will be processed. *

*

* The query will only be modified if 1) it is capable of returning keys (ie * INSERT, DELETE and UPDATE) and 2) does not already contain a RETURNING * clause. *

* * @param sql * SQL statement * @param autoGeneratedKeys * Valid values {@link java.sql.Statement#NO_GENERATED_KEYS} and * {@link java.sql.Statement#RETURN_GENERATED_KEYS} * @throws SQLException * If the supplied autoGeneratedKeys value does not match valid * values or if the parser cannot be loaded when autoGeneratedKeys = RETURN_GENERATED_KEYS. */ public AbstractGeneratedKeysQuery(String sql, int autoGeneratedKeys) throws SQLException { this(sql); switch (autoGeneratedKeys) { case Statement.RETURN_GENERATED_KEYS: if (parser == null) { throw new FBDriverNotCapableException("Generated keys functionality not available, most likely cause: ANTLR-Runtime not available on classpath"); } queryType = QUERY_TYPE_ADD_ALL_COLUMNS; break; case Statement.NO_GENERATED_KEYS: queryType = QUERY_TYPE_KEEP_UNMODIFIED; break; default: throw new FBSQLException("Supplied value for autoGeneratedKeys is invalid", FBSQLException.SQL_STATE_INVALID_OPTION_IDENTIFIER); } } /** * Process SQL statement for adding generated key columns by their ordinal * position. *

* The query will only be modified if 1) it is capable of returning keys (ie * INSERT, DELETE and UPDATE) and 2) does not already contain a RETURNING * clause. *

*

* The columns are added in ascending order of their index value, not by the * order of indexes in the columnIndexes array. The values of columnIndexes * are taken as the ORDINAL_POSITION returned by * {@link java.sql.DatabaseMetaData#getColumns(String, String, String, String)} * . When a column index does not exist for the table of the query, then it * will be discarded from the list silently. *

* * @param sql * SQL statement * @param columnIndexes * Array of ORDINAL_POSITION values of the columns to return as * generated key * @throws SQLException If the parser cannot be loaded */ public AbstractGeneratedKeysQuery(String sql, int[] columnIndexes) throws SQLException { this(sql); if (parser == null) { throw new FBDriverNotCapableException("Generated keys functionality not available, most likely cause: ANTLR-Runtime not available on classpath"); } else if (columnIndexes != null && columnIndexes.length != 0) { this.columnIndexes = columnIndexes.clone(); queryType = QUERY_TYPE_ADD_INDEXED; } else { queryType = QUERY_TYPE_KEEP_UNMODIFIED; } } /** * Process SQL statement for adding generated key columns by name. *

* The query will only be modified if 1) it is capable of returning keys (ie * INSERT, DELETE and UPDATE) and 2) does not already contain a RETURNING * clause. *

*

* The columnNames passed are taken as is and included in a new returning * clause. There is no check for actual existence of these columns, nor are * they quoted. *

* * @param sql * SQL statement * @param columnNames * Array of column names to return as generated key * @throws SQLException If the parser cannot be loaded */ public AbstractGeneratedKeysQuery(String sql, String[] columnNames) throws SQLException { this(sql); if (parser == null) { throw new FBDriverNotCapableException("Generated keys functionality not available, most likely cause: ANTLR-Runtime not available on classpath"); } else if (columnNames != null && columnNames.length != 0) { this.columnNames = columnNames.clone(); queryType = QUERY_TYPE_ADD_COLUMNS; } else { queryType = QUERY_TYPE_KEEP_UNMODIFIED; } } /** * Indicates if the query will generate keys. * * @return true if the query will generate keys, * false otherwise * @throws SQLException * For errors accessing the metadata */ public boolean generatesKeys() throws SQLException { process(); return generatesKeys; } /** * Returns the actual query. *

* Use {@link #generatesKeys()} to see if this query will in fact generate * keys. *

* * @return The SQL query * @throws SQLException * For errors accessing the metadata */ public String getQueryString() throws SQLException { process(); return modifiedSQL; } /** * Parses the query and updates the query with generated keys if * modifications are needed or possible. * * @throws SQLException * For errors accessing the metadata */ private void process() throws SQLException { if (processed) { return; } try { processStatementModel(); updateQuery(); } finally { processed = true; } } /** * Parses the original SQL query and checks if it already has a RETURNING * clause * @throws SQLException If query parsing is not available */ private void processStatementModel() throws SQLException { if (parser == null) { if (queryType == QUERY_TYPE_KEEP_UNMODIFIED) { // JDBC specifies that NO_GENERATED_KEYS (signified here by QUERY_TYPE_KEEP_UNMODIFIED) // should not result in failure if processing generated keys is not possible return; } else { // This condition should already have been caught in constructors, but do it anyway: throw new FBDriverNotCapableException("Generated keys functionality not available, most likely cause: ANTLR-Runtime not available on classpath"); } } try { statementModel = parseInsertStatement(originalSQL); if (statementModel.getReturningColumns().size() > 0) { queryType = QUERY_TYPE_ALREADY_HAS_RETURNING; } } catch (ParseException e) { // Unrecognized statement (so no INSERT, DELETE, UPDATE or UPDATE OR // INSERT statement), keep as is queryType = QUERY_TYPE_KEEP_UNMODIFIED; } } /** * Adds the generated key columns to the query. * * @throws SQLException * For errors accessing the metadata */ private void updateQuery() throws SQLException { switch (queryType) { case QUERY_TYPE_ADD_ALL_COLUMNS: addAllColumns(); break; case QUERY_TYPE_ADD_INDEXED: addIndexedColumns(); break; case QUERY_TYPE_ADD_COLUMNS: addReturningClause(); break; case QUERY_TYPE_ALREADY_HAS_RETURNING: generatesKeys = true; queryType = QUERY_TYPE_KEEP_UNMODIFIED; break; case QUERY_TYPE_KEEP_UNMODIFIED: // Do nothing break; default: throw new IllegalStateException("Unsupported value for queryType: " + queryType); } // Not part of switch: elements of switch will modify queryType (eg when // nothing is added) if (queryType == QUERY_TYPE_KEEP_UNMODIFIED) { modifiedSQL = originalSQL; } } /** * Adds all available table columns to the query as generated keys. * * @throws SQLException * For errors accessing the metadata */ private void addAllColumns() throws SQLException { DatabaseMetaData metaData = getDatabaseMetaData(); ResultSet rs = metaData.getColumns(null, null, statementModel.getTableName(), null); List columns = new ArrayList(); try { while (rs.next()) { // Need to quote columns for mixed case columns columns.add('"' + rs.getString(4) + '"'); } } finally { rs.close(); } columnNames = (String[]) columns.toArray(new String[0]); addReturningClause(); } /** * Adds all columns referenced by columnIndexes to the query as generated * keys. * * @throws SQLException * For errors accessing the metadata */ private void addIndexedColumns() throws SQLException { DatabaseMetaData metaData = getDatabaseMetaData(); ResultSet rs = metaData.getColumns(null, null, statementModel.getTableName(), null); Arrays.sort(columnIndexes); List columns = new ArrayList(); try { while (rs.next()) { if (Arrays.binarySearch(columnIndexes, rs.getInt(17)) >= 0) { // Need to quote columns for mixed case columns columns.add('"' + rs.getString(4) + '"'); } } } finally { rs.close(); } columnNames = (String[]) columns.toArray(new String[0]); addReturningClause(); } /** * Adds the columns in columnNames to the query as generated keys. */ private void addReturningClause() { if (columnNames == null || columnNames.length == 0) { queryType = QUERY_TYPE_KEEP_UNMODIFIED; return; } generatesKeys = true; StringBuffer query = new StringBuffer(originalSQL); if (query.charAt(query.length() - 1) == ';') { query.deleteCharAt(query.length() - 1); } query.append('\n'); query.append("RETURNING "); for (int i = 0; i < columnNames.length; i++) { query.append(columnNames[i]); if (i < columnNames.length - 1) { query.append(", "); } } modifiedSQL = query.toString(); } /** * Returns the DatabaseMetaData object to be used when processing this * query. In general this should be a DatabaseMetaData object created from * the connection which will execute the query. * * @return DatabaseMetaData object * @throws SQLException * if a database access error occurs */ abstract DatabaseMetaData getDatabaseMetaData() throws SQLException; /** * Parse the INSERT statement and extract the corresponding model. * * @param sql * SQL statement to parse. * * @return instance of {@link JaybirdStatementModel} * @throws ParseException if statement cannot be parsed. */ private JaybirdStatementModel parseInsertStatement(String sql) throws ParseException { return parser.parseInsertStatement(sql); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy