View Javadoc

1   ////////////////////////////////////////////////////////////////////////////////
2   // MillScript: an Open Spice interpreter and batch website creation tool
3   // Copyright (C) 2001-2004 Open World Ltd
4   // Copyright (C) 2005 Kevin Rogers
5   //
6   // This file is part of MillScript.
7   //
8   // MillScript is free software; you can redistribute it and/or modify it under
9   // the terms of the GNU General Public License as published by the Free
10  // Software Foundation; either version 2 of the License, or (at your option)
11  // any later version.
12  //
13  // MillScript is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  // FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16  // more details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // MillScript; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA  02111-1307  USA
21  ////////////////////////////////////////////////////////////////////////////////
22  package org.millscript.millscript.functions;
23  
24  import org.millscript.commons.util.list.ELinkedList;
25  import org.millscript.millscript.alert.Alerts;
26  import org.millscript.millscript.datatypes.DatabaseRecord;
27  import org.millscript.millscript.datatypes.DatabaseSource;
28  import org.millscript.millscript.vm.Machine;
29  
30  import java.sql.Connection;
31  import java.sql.Date;
32  import java.sql.PreparedStatement;
33  import java.sql.ResultSet;
34  import java.sql.ResultSetMetaData;
35  import java.sql.SQLException;
36  import java.util.GregorianCalendar;
37  
38  /**
39   * This class implements a millscript function to execute an SQL query through
40   * the JDBC interface. The query is done using a prepared statement, which
41   * should improve efficiency for multiple queries. The generated function
42   * requires at least one argument, which will be the DataSource for the query.
43   * Any subsequent arguments are treated as parameters for the prepared
44   * statement.
45   * <pre>
46   * f( datasource, arg1, arg2, arg3, ... )
47   * </pre>
48   * The function returns a list-of-maps.
49   *
50   * @since 9.6.4
51   */
52  public class JdbcExecuteQueryFunction extends Function {
53  
54      /**
55       * The SQL statement this function will prepare and execute.
56       */
57      private final String sql;
58  
59      /**
60       * The Connection, cached when the function is first appplied, or the
61       * DataSource changes.
62       */
63      private Connection cachedConn;
64  
65      /**
66       * The PreparedStatement, cached when the function is first applied, or the
67       * DataSource changes.
68       */
69      private PreparedStatement cachedPrep;
70  
71      /**
72       * Construct a new function which will execute the specifed SQL as a
73       * prepared statement.
74       *
75       * @param s the string containing SQL to prepare and execute
76       */
77      public JdbcExecuteQueryFunction( final String s ) {
78          this.sql = s;
79      }
80  
81      /**
82       * @see org.millscript.millscript.functions.Function#apply(org.millscript.millscript.vm.Machine, int)
83       */
84      @Override
85      public void apply( final Machine mc, final int nargs ) {
86  
87          // Check that we received at least one argument
88          this.checkNargsGT( mc, 1, nargs );
89  
90          // Get any prepared statement arguments
91          final Object[] prepArgs = mc.popArgsArray( nargs - 1 );
92  
93          // Pop the DataSource we will use for executing the prepared statement
94          final DatabaseSource source = mc.popDatabaseSource();
95  
96          try {
97  
98              // Obtain a connection from the supplied DataSource
99              Connection conn = source.getConnection();
100 
101             // First check if the supplied DataSource is the same as the one we
102             // have cached from previous applications of this function.
103             if ( conn != cachedConn ) {
104                 // Close the existing prepared statement and connection.
105                 if ( cachedPrep != null ) {
106                     cachedPrep.close();
107                 }
108 
109                 // Using the new connection, prepare the SQL statement
110                 cachedPrep = conn.prepareStatement( sql );
111 
112                 // Now we're fully initialised, store the supplied DataSource.
113                 cachedConn = conn;
114             }
115 
116             // Initialise the parameters for the prepared statement by iterating
117             // through each one and setting it.
118             for ( int i = 0; i < prepArgs.length; i++ ) {
119                 this.cachedPrep.setObject( i + 1, prepArgs[ i ] );
120             }
121 
122             ResultSet rs = this.cachedPrep.executeQuery();
123 
124             // Push the results from the execution of the statement.
125             mc.pushObject( changeResultSet( rs ) );
126 
127             // Close the result set to release resources
128             rs.close();
129 
130             // Clear any parameters from the prepared statement.
131             this.cachedPrep.clearParameters();
132 
133         } catch ( SQLException ex ) {
134             // We would want to report the SQL state and the PostgreSQL vendor
135             // error code as commented out below, except they don't provide it.
136             // culprit( "SQLState", ex.getSQLState() ),
137             // culprit( "PostgreSQL error code", String.valueOf( ex.getErrorCode() ) ),
138             throw(
139                 Alerts.eval(
140                     "SQL query failure",
141                     ex.getMessage()
142                 ).culprit( "SQL", sql ).mishap()
143             );
144         }
145 
146     }
147 
148     /**
149      * Changes the supplied ResultSet into a LinkedList of database records.
150      *
151      * @param   rs  the result set containing the results to change
152      * @return  a LinkedList of database records
153      */
154     private ELinkedList changeResultSet( final ResultSet rs ) throws SQLException {
155 
156         // The meta data describing the columns in the result set
157         final ResultSetMetaData meta = rs.getMetaData();
158 
159         // The list of database records we will return
160         final ELinkedList< DatabaseRecord > results = new ELinkedList< DatabaseRecord >();
161 
162         // The number of columns in a row
163         final int columnCount = meta.getColumnCount();
164 
165         // Iterate through each row in the result set
166         while ( rs.next() ) {
167 
168             // A DatabaseRecord represents a single row in the results of the
169             // SQL query.
170             final DatabaseRecord record = new DatabaseRecord();
171 
172             // We must unlock the record so that we can insert keys that do not
173             // start with "!"
174             record.unlock();
175 
176             // Loop through each column in the row
177             for ( int index = 1; index <= columnCount; index++ ) {
178 
179                 // Get the column name
180                 final String columnName = meta.getColumnName( index );
181 
182                 // Perform any special processing depending on the type of data
183                 // held in the column.
184                 switch ( meta.getColumnType( index ) ) {
185 
186                     case java.sql.Types.DATE:
187 
188                         final GregorianCalendar cal = new GregorianCalendar();
189                         Date date = rs.getDate( index, cal );
190 
191                         if ( date != null ) {
192                             cal.setTime( date );
193                         }
194 
195                         record.insert( columnName, cal );
196                         break;
197 
198                     default:
199                         record.insert( columnName, rs.getObject( index ) );
200 
201                 }
202 
203             }
204 
205             // We must lock the record so that the original data from the
206             // database is protected.
207             record.lock();
208 
209             // Add the record to the list of database records to return.
210             results.addLast( record );
211 
212         }
213 
214         int size = results.size();
215 
216         System.out.println( "Received " + size + " record" + ( size == 1 ? "" : "s" ) );
217 
218         // Return the list of database records
219         return results;
220     }
221 
222     /**
223      * Returns a string representation of this function. It shows that it
224      * requires one or more arguments.
225      *
226      * @return    a string representation of this function
227      */
228     @Override
229     public String toString() {
230         String name = getName();
231         return (
232             name == null ?
233             "<function (1+)>" :
234             "<function " + name + "(1+)>"
235         );
236     }
237 
238 }