View Javadoc

1   /*
2    * $Id: JdbcPlugin.java,v 1.7 2006/09/26 15:34:42 clq2 Exp $
3    *
4    * (C) Copyright Astrogrid...
5    */
6   
7   package org.astrogrid.tableserver.jdbc;
8   
9   import org.astrogrid.dataservice.queriers.*;
10  
11  import java.io.IOException;
12  import java.lang.reflect.InvocationTargetException;
13  import java.security.Principal;
14  import java.sql.Connection;
15  import java.sql.ResultSet;
16  import java.sql.SQLException;
17  import java.sql.Statement;
18  import java.util.Date;
19  import org.astrogrid.cfg.ConfigFactory;
20  import org.astrogrid.tableserver.metadata.TableMetaDocInterpreter;
21  import org.astrogrid.dataservice.queriers.status.QuerierComplete;
22  import org.astrogrid.dataservice.queriers.status.QuerierError;
23  import org.astrogrid.dataservice.queriers.status.QuerierQuerying;
24  import org.astrogrid.query.Query;
25  import org.astrogrid.query.QueryException;
26  
27  /***
28   * A general purpose SQL Querier that will (hopefully) produce bog standard
29   * realbasic SQL from the ADQL, throwing an exception if it can't be done
30   *
31   * <p>
32   * forms a basis for other implementations for different db flavours
33   * <p>
34   * NWW: altered to delay creating jdbcConnection until required by {@link #queryDatabase}. DatabaseQueriers are one-shot
35   * beasts anyhow, so this isn't a problem, but it fixes problems of moving jdbcConnection across threads when non-blocking querying is done.
36   * <p>
37   *  * @author M Hill
38   */
39  
40  public class JdbcPlugin extends DefaultPlugin {
41     
42     
43     /*** Adql -> SQL translator class */
44     public static final String SQL_TRANSLATOR = "datacenter.querier.plugin.sql.translator";
45     public static final String DEFAULT_SQL_TRANSLATOR = "org.astrogrid.tableserver.jdbc.AdqlSqlMaker";
46     
47     /*** execute timeout  */
48     public static final String TIMEOUT = "datacenter.sql.timeout";
49  
50     /*** Connection manager */
51     private static JdbcConnections connectionManager = null;
52     
53  
54     /*** performs a synchronous call to the database, submitting the given query
55      * in sql form and retiirning the results as a SqlResults wrapper arond the JDBC result set.
56      * @param o a string
57      */
58     public void askQuery(Principal user, Query query, Querier querier) throws IOException, QueryException {
59  
60        validateQuery(query);
61        
62        String sql = "(not set)";
63        Connection jdbcConnection = null;
64        
65        try {
66           //convert to SQL
67           log.debug("Making SQL");
68           SqlMaker sqlMaker = makeSqlMaker();
69           sql = sqlMaker.makeSql(query);
70           
71           querier.setStatus(new QuerierQuerying(querier.getStatus(), sql));
72  
73           if ((sql == null) || (sql.length() == 0)) {
74              throw new QueryException("SqlMaker returned empty SQL string for query "+query);
75           }
76           
77  //done in querierQuerying above         querier.getStatus().addDetail("SQL: "+sql);
78        
79           //connect to database
80           log.debug("Connecting to the database");
81           jdbcConnection = getJdbcConnection();
82           //Statement statement = jdbcConnection.createStatement();
83           Statement statement = jdbcConnection.createStatement(
84               java.sql.ResultSet.TYPE_FORWARD_ONLY,
85               java.sql.ResultSet.CONCUR_READ_ONLY);
86  
87           // make sure autocommit is off
88           // Postgres seems to need this to stop it downloading the 
89           // whole ResultSet in one go.
90           jdbcConnection.setAutoCommit(false);
91  
92           //limit to number of rows returned
93           // KEA says: This is the WRONG way to set the query limit - it 
94           // doesn't restrict the actual sql query made to the database, it 
95           // just silently drops any excess rows.  So "select *" would cause 
96           // an unrestricted query to be run on the DBMS, even if only the
97           // first 100 rows were actually captured by the JDBC results.
98           // Disastrous!
99           // In the query class, the smaller of the query limit and local
100          // limit is used in the sql translation process, so it produces
101          // a properly restricted query.
102          //
103          // I am leaving this clause in as an extra safety measure, in
104          // case something goes awry with a query and it spews back too
105          // many results.
106          if (query.getLocalLimit() >0) { 
107             statement.setMaxRows((int) query.getLocalLimit()); 
108          }
109          
110          //set timeout - 0 = no limit
111          statement.setQueryTimeout(ConfigFactory.getCommonConfig().getInt(TIMEOUT, 30*60)); //default to half an hour
112 
113          querier.getStatus().addDetail("Submitted to JDBC at "+new Date());
114 
115          try {
116            // We were having problems with the JDBC driver trying to fetch all
117            // the data at once (rather than chunking it) and running out of
118            // memory as a result.
119            // Got this suggestion here: 
120            // http://jira.jboss.com/jira/browse/JBAS-1336
121            // The negative value is required to provoke sensible chunking data 
122            // retrieval by MySQL, and works with SQLServer  (values >0 here
123            // still cause MySQL to try to grab the whole lot, apparently)
124             statement.setFetchSize(Integer.MIN_VALUE);
125          }
126          catch (SQLException e) {
127            // Some JDBC drivers (e.g. PostgreSQL >= 8.0) object to the
128            // negative value in setFetchSize() above, so let's try again 
129            // with a small positive value as a fallback
130             try {
131                statement.setFetchSize(1000);
132             }
133             catch (SQLException e2) {
134            // This method isn't implemented at all in some JDBC drivers, e.g.
135            // PostgreSQL <8.0.   
136                log.info("Couldn't set JDBC fetch size: " + e2.getMessage()); 
137             }
138          }  
139          //execute query
140          log.info("Performing Query: " + sql);
141          statement.execute(sql);
142          querier.getStatus().addDetail("JDBC execution complete at "+new java.util.Date());
143 
144          ResultSet results = statement.getResultSet();
145          
146          if (!aborted) {
147             
148             if (results == null) {
149                throw new QueryException("SQL '"+sql+"' returned null results");
150             }
151             
152             //sort out results
153             TableResults qResults = makeSqlResults(querier, results);
154             qResults.send(query.getResultsDef(), querier.getUser());
155             
156          }
157          //don't do this as some dbs seem to want to cycle through the lot. Let the garbage collector handle it
158          //results.close();
159          
160       }
161       catch (SQLException e) {
162          log.error("SQLException when querying database with query  " + sql);
163          log.error("Exception is :" + e.toString());
164          querier.setStatus(new QuerierError(querier.getStatus(), "JDBC Query Failed",e));
165          //we don't really need to store stack info for the SQL exception, which saves logging...
166          throw new DatabaseAccessException(e+" using '" + sql + "': ",e);
167       }
168       finally {
169          //try to tidy up now
170          try {
171             if (jdbcConnection != null) { jdbcConnection.close(); }
172          } catch (SQLException e) { } //ignore
173       }
174 
175    }
176    
177    /*** Returns just the number of matches rather than the list of matches */
178    public long getCount(Principal user, Query query, Querier querier) throws IOException, QueryException {
179 
180       validateQuery(query);
181       
182       String sql = "(not set)";
183       Connection jdbcConnection = null;
184       
185       try {
186          //convert to SQL
187          SqlMaker sqlMaker = makeSqlMaker();
188 
189          sql = sqlMaker.makeCountSql(query);
190 
191          querier.setStatus(new QuerierQuerying(querier.getStatus(), sql));
192          
193          if ((sql == null) || (sql.length() == 0)) {
194             throw new QueryException("SqlMaker returned empty SQL string for query "+query);
195          }
196          
197          //connect to database
198          log.debug("Connecting to the database");
199          jdbcConnection = getJdbcConnection();
200          Statement statement = jdbcConnection.createStatement();
201          
202          querier.getStatus().addDetail("Submitted to JDBC at "+new Date());
203          
204          //execute query
205          log.info("Performing Query: " + sql);
206          statement.execute(sql);
207          querier.getStatus().addDetail("JDBC execution complete at "+new java.util.Date());
208 
209          ResultSet results = statement.getResultSet();
210 
211          //count is the first row first column
212          results.next();
213          long count = results.getLong(1);
214          results.close();
215 
216          querier.getStatus().addDetail("Count="+count);
217          querier.setStatus(new QuerierComplete(querier.getStatus()));
218          
219          return count;
220       }
221       catch (SQLException e) {
222          log.error("SQLException when querying database with query  " + sql);
223          log.error("Exception is :" + e.toString());
224          querier.setStatus(new QuerierError(querier.getStatus(), "JDBC Query Failed",e));
225          //we don't really need to store stack info for the SQL exception, which saves logging...
226          throw new DatabaseAccessException(e+" using '" + sql + "': ",e);
227       }
228       finally {
229          //try to tidy up now
230          try {
231             if (jdbcConnection != null) { jdbcConnection.close(); }
232          } catch (SQLException e) { } //ignore
233       }
234 
235    }
236 
237    /*** Throws an IllegalArgumentException if the query is not appropriate to this site */
238    public void validateQuery(Query query) {
239       try {
240          TableMetaDocInterpreter reader = new TableMetaDocInterpreter();
241          RdbmsQueryValidator validator = new RdbmsQueryValidator(reader);
242          //query.acceptVisitor(validator); //throws an IllegalArgumentException if there's something wrong
243          validator.validateQuery(query);
244       }
245       catch (IOException ioe) {
246          log.warn("No RDBMS Resource found, not validating query");
247       }
248    }
249    
250    /*** Returns the formats that this plugin can provide.  Asks the results class; override in subclasse if nec */
251    public String[] getFormats() {
252       return SqlResults.listFormats();
253    }
254    
255    /*** Makes SqlResults for the resultset.  This means subclasses can override it
256     * to make an easy way to transform the results */
257    public TableResults makeSqlResults(Querier querier, ResultSet results) {
258       return new SqlResults(querier, results);
259    }
260    
261    /***
262     * Makes the right SqlQueryMaker for this database
263     */
264    public SqlMaker makeSqlMaker() throws QuerierPluginException {
265       String makerClass = ConfigFactory.getCommonConfig().getString(
266             SQL_TRANSLATOR, DEFAULT_SQL_TRANSLATOR);
267       
268       try {
269          Object o = QuerierPluginFactory.instantiate(makerClass);
270          if (o == null) {
271             throw new QuerierPluginException("Could not create the SQL plugin translator '"+makerClass+"'");
272          }
273          return (SqlMaker) o;
274       }
275       catch (ClassCastException cce) {
276          String msg = "The class '"+makerClass+
277            "' (specified in configuration key '"+SQL_TRANSLATOR+
278            "') is not a subclass of " + SqlMaker.class.getName()+
279            "; please check your configuration";
280          log.error(msg, cce);
281          throw new QuerierPluginException(msg, cce);
282       }
283       catch (ClassNotFoundException e) {
284          String msg = "Could not find class '"+makerClass+
285            "' (specified in configuration key '"+SQL_TRANSLATOR+
286            "'); please check your configuration.";
287          log.error(msg, e);
288          throw new QuerierPluginException(msg, e.getCause());
289       }
290       catch (Throwable th) {
291          if (th instanceof InvocationTargetException) {
292             th = th.getCause();  //extract cause - don't care about the invocation bit
293          }
294          String msg = "Problem instantiating SQL Maker "+makerClass+", config key="+SQL_TRANSLATOR+", please see logs for more information";
295          log.error(msg, th);
296          log.error(msg, th.getCause());
297          throw new QuerierPluginException(msg, th);
298       }
299    }
300    
301    /*** Creates a connection to the database */
302    protected static synchronized Connection getJdbcConnection() throws IOException, SQLException {
303       
304       if (connectionManager == null) {
305          connectionManager = JdbcConnections.makeFromConfig();
306       }
307       return connectionManager.createConnection();
308 
309    }
310 
311    
312    
313    
314 }
315 
316 
317 
318