001    /*
002    // $Id: //open/mondrian/src/main/mondrian/rolap/SqlStatement.java#5 $
003    // This software is subject to the terms of the Common Public License
004    // Agreement, available at the following URL:
005    // http://www.opensource.org/licenses/cpl.html.
006    // Copyright (C) 2007-2008 Julian Hyde and others
007    // All Rights Reserved.
008    // You must accept the terms of that agreement to use this software.
009    */
010    package mondrian.rolap;
011    
012    import mondrian.olap.Util;
013    
014    import javax.sql.DataSource;
015    
016    import java.sql.Connection;
017    import java.sql.ResultSet;
018    import java.sql.SQLException;
019    import java.sql.Statement;
020    
021    /**
022     * SqlStatement contains a SQL statement and associated resources throughout
023     * its lifetime.
024     *
025     * <p>The goal of SqlStatement is to make tracing, error-handling and
026     * resource-management easier. None of the methods throws a SQLException;
027     * if an error occurs in one of the methods, the method wraps the exception
028     * in a {@link RuntimeException} describing the high-level operation, logs
029     * that the operation failed, and throws that RuntimeException.
030     *
031     * <p>If methods succeed, the method generates lifecycle logging such as
032     * the elapsed time and number of rows fetched.
033     *
034     * <p>There are a few obligations on the caller. The caller must:<ul>
035     * <li>call the {@link #handle(Exception)} method if one of the contained
036     *     objects (say the {@link java.sql.ResultSet}) gives an error;
037     * <li>call the {@link #close()} method if all operations complete
038     *     successfully.
039     * <li>increment the {@link #rowCount} field each time a row is fetched.
040     * </ul>
041     *
042     * <p>The {@link #close()} method is idempotent. You are welcome to call it
043     * more than once.
044     *
045     * <p>SqlStatement is not thread-safe.
046     *
047     * @version $Id: //open/mondrian/src/main/mondrian/rolap/SqlStatement.java#5 $
048     * @author jhyde
049     * @since 2.3
050     */
051    public class SqlStatement {
052    
053        private final DataSource dataSource;
054        private Connection jdbcConnection;
055        private ResultSet resultSet;
056        private final String sql;
057        private final int maxRows;
058        private final String component;
059        private final int resultSetType;
060        private final int resultSetConcurrency;
061        private final RolapUtil.Semaphore querySemaphore = RolapUtil
062            .getQuerySemaphore();
063        private final String message;
064        private boolean haveSemaphore;
065        public int rowCount;
066        private long startTime;
067    
068        // used for SQL logging, allows for a SQL Statement UID
069        private static long executeCount = -1;
070    
071        SqlStatement(
072            DataSource dataSource,
073            String sql,
074            int maxRows,
075            String component,
076            String message,
077            int resultSetType,
078            int resultSetConcurrency)
079        {
080            this.dataSource = dataSource;
081            this.sql = sql;
082            this.maxRows = maxRows;
083            this.component = component;
084            this.message = message;
085            this.resultSetType = resultSetType;
086            this.resultSetConcurrency = resultSetConcurrency;
087        }
088    
089        public void execute() throws SQLException {
090            this.jdbcConnection = dataSource.getConnection();
091            querySemaphore.enter();
092            haveSemaphore = true;
093            Statement statement = null;
094            String status = "failed";
095            long currId = 0;
096            // Trace start of execution.
097            if (RolapUtil.SQL_LOGGER.isDebugEnabled()) {
098                currId = ++executeCount;
099                StringBuffer sqllog = new StringBuffer();
100                sqllog.append(currId + ": " + component + ": executing sql [");
101                if (sql.indexOf('\n') >= 0) {
102                    // SQL appears to be formatted as multiple lines. Make it
103                    // start on its own line.
104                    sqllog.append("\n");
105                }
106                sqllog.append(sql);
107                sqllog.append(']');
108                RolapUtil.SQL_LOGGER.debug(sqllog.toString());
109            }
110    
111            // Execute hook.
112            RolapUtil.ExecuteQueryHook hook = RolapUtil.threadHooks.get();
113            if (hook != null) {
114                hook.onExecuteQuery(sql);
115            }
116            try {
117                startTime = System.currentTimeMillis();
118                if (resultSetType < 0 || resultSetConcurrency < 0) {
119                    statement = jdbcConnection.createStatement();
120                } else {
121                    statement = jdbcConnection.createStatement(
122                        resultSetType,
123                        resultSetConcurrency);
124                }
125                if (maxRows > 0) {
126                    statement.setMaxRows(maxRows);
127                }
128                this.resultSet = statement.executeQuery(sql);
129                long time = System.currentTimeMillis();
130                final long execMs = time - startTime;
131                Util.addDatabaseTime(execMs);
132                status = ", exec " + execMs + " ms";
133            } catch (SQLException e) {
134                status = ", failed (" + e + ")";
135                try {
136                    if (statement != null) {
137                        statement.close();
138                    }
139                } catch (SQLException e2) {
140                    // ignore
141                }
142                throw handle(e);
143            } finally {
144                RolapUtil.SQL_LOGGER.debug(currId + ": " + status);
145    
146                if (RolapUtil.LOGGER.isDebugEnabled()) {
147                    RolapUtil.LOGGER.debug(component + ": executing sql [" +
148                        sql + "]" + status);
149                }
150            }
151        }
152    
153        /**
154         * Closes all resources (statement, result set) held by this
155         * SqlStatement.
156         *
157         * <p>If any of them fails, wraps them in a
158         * {@link RuntimeException} describing the high-level operation which
159         * this statement was performing. No further error-handling is required
160         * to produce a descriptive stack trace, unless you want to absorb the
161         * error.
162         */
163        public void close() {
164            if (haveSemaphore) {
165                haveSemaphore = false;
166                querySemaphore.leave();
167            }
168            if (resultSet != null) {
169                try {
170                    resultSet.close();
171                } catch (SQLException e) {
172                    throw Util.newError(message + "; sql=[" + sql + "]");
173                } finally {
174                    resultSet = null;
175                }
176            }
177            if (jdbcConnection != null) {
178                try {
179                    jdbcConnection.close();
180                } catch (SQLException e) {
181                    throw Util.newError(message + "; sql=[" + sql + "]");
182                } finally {
183                    jdbcConnection = null;
184                }
185            }
186            long time = System.currentTimeMillis();
187            long totalMs = time - startTime;
188            String status = ", exec+fetch " + totalMs + " ms, " + rowCount + " rows";
189    
190            RolapUtil.SQL_LOGGER.debug(executeCount + ": " + status);
191    
192            if (RolapUtil.LOGGER.isDebugEnabled()) {
193                RolapUtil.LOGGER.debug(component + ": done executing sql [" +
194                    sql + "]" + status);
195            }
196        }
197    
198        public ResultSet getResultSet() {
199            return resultSet;
200        }
201    
202        /**
203         * Handles an exception thrown from the ResultSet, implicitly calls
204         * {@link #close}, and returns an exception which includes the full
205         * stack, including a description of the high-level operation.
206         *
207         * @param e Exception
208         * @return Runtime exception
209         */
210        public RuntimeException handle(Exception e) {
211            RuntimeException runtimeException =
212                Util.newError(e, message + "; sql=[" + sql + "]");
213            try {
214                close();
215            } catch (RuntimeException re) {
216                // ignore
217            }
218            return runtimeException;
219        }
220    }
221    
222    // End SqlStatement.java