View Javadoc

1   package org.apache.torque.task;
2   
3   /*
4    * Copyright 2001-2005 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License")
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import java.io.BufferedOutputStream;
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.InputStreamReader;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileOutputStream;
26  import java.io.FileReader;
27  import java.io.PrintStream;
28  import java.io.StringReader;
29  import java.io.Reader;
30  import java.util.List;
31  import java.util.ArrayList;
32  import java.util.Iterator;
33  import java.util.HashMap;
34  import java.util.Map;
35  import java.util.Properties;
36  import java.sql.Connection;
37  import java.sql.DatabaseMetaData;
38  import java.sql.Driver;
39  import java.sql.ResultSet;
40  import java.sql.ResultSetMetaData;
41  import java.sql.SQLException;
42  import java.sql.SQLWarning;
43  import java.sql.Statement;
44  import org.apache.commons.lang.StringUtils;
45  import org.apache.tools.ant.AntClassLoader;
46  import org.apache.tools.ant.BuildException;
47  import org.apache.tools.ant.Project;
48  import org.apache.tools.ant.ProjectHelper;
49  import org.apache.tools.ant.Task;
50  import org.apache.tools.ant.types.EnumeratedAttribute;
51  import org.apache.tools.ant.types.Path;
52  import org.apache.tools.ant.types.Reference;
53  
54  /***
55   * This task uses an SQL -> Database map in the form of a properties
56   * file to insert each SQL file listed into its designated database.
57   *
58   * @author <a href="mailto:jeff@custommonkey.org">Jeff Martin</a>
59   * @author <a href="mailto:gholam@xtra.co.nz">Michael McCallum</A>
60   * @author <a href="mailto:tim.stephenson@sybase.com">Tim Stephenson</A>
61   * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</A>
62   * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
63   * @version $Id: TorqueSQLExec.java 239624 2005-08-24 12:18:03Z henning $
64   */
65  public class TorqueSQLExec extends Task
66  {
67      private int goodSql = 0;
68      private int totalSql = 0;
69      private Path classpath;
70      private AntClassLoader loader;
71  
72      /***
73       *
74       */
75      public static class DelimiterType extends EnumeratedAttribute
76      {
77          public static final String NORMAL = "normal";
78          public static final String ROW = "row";
79  
80          public String[] getValues()
81          {
82              return new String[] {NORMAL, ROW};
83          }
84      }
85  
86      /*** Database connection */
87      private Connection conn = null;
88  
89      /*** Autocommit flag. Default value is false */
90      private boolean autocommit = false;
91  
92      /*** SQL statement */
93      private Statement statement = null;
94  
95      /*** DB driver. */
96      private String driver = null;
97  
98      /*** DB url. */
99      private String url = null;
100 
101     /*** User name. */
102     private String userId = null;
103 
104     /*** Password */
105     private String password = null;
106 
107     /*** SQL input command */
108     private String sqlCommand = "";
109 
110     /*** SQL Statement delimiter */
111     private String delimiter = ";";
112 
113     /***
114      * The delimiter type indicating whether the delimiter will
115      * only be recognized on a line by itself
116      */
117     private String delimiterType = DelimiterType.NORMAL;
118 
119     /*** Print SQL results. */
120     private boolean print = false;
121 
122     /*** Print header columns. */
123     private boolean showheaders = true;
124 
125     /*** Results Output file. */
126     private File output = null;
127 
128     /*** RDBMS Product needed for this SQL. */
129     private String rdbms = null;
130 
131     /*** RDBMS Version needed for this SQL. */
132     private String version = null;
133 
134     /*** Action to perform if an error is found */
135     private String onError = "abort";
136 
137     /*** Encoding to use when reading SQL statements from a file */
138     private String encoding = null;
139 
140     /*** Src directory for the files listed in the sqldbmap. */
141     private String srcDir;
142 
143     /*** Properties file that maps an individual SQL file to a database. */
144     private File sqldbmap;
145 
146     /***
147      * Set the sqldbmap properties file.
148      *
149      * @param sqldbmap filename for the sqldbmap
150      */
151     public void setSqlDbMap(String sqldbmap)
152     {
153         this.sqldbmap = project.resolveFile(sqldbmap);
154     }
155 
156     /***
157      * Get the sqldbmap properties file.
158      *
159      * @return filename for the sqldbmap
160      */
161     public File getSqlDbMap()
162     {
163         return sqldbmap;
164     }
165 
166     /***
167      * Set the src directory for the sql files listed in the sqldbmap file.
168      *
169      * @param srcDir sql source directory
170      */
171     public void setSrcDir(String srcDir)
172     {
173         this.srcDir = project.resolveFile(srcDir).toString();
174     }
175 
176     /***
177      * Get the src directory for the sql files listed in the sqldbmap file.
178      *
179      * @return sql source directory
180      */
181     public String getSrcDir()
182     {
183         return srcDir;
184     }
185 
186     /***
187      * Set the classpath for loading the driver.
188      *
189      * @param classpath the classpath
190      */
191     public void setClasspath(Path classpath)
192     {
193         if (this.classpath == null)
194         {
195             this.classpath = classpath;
196         }
197         else
198         {
199             this.classpath.append(classpath);
200         }
201     }
202 
203     /***
204      * Create the classpath for loading the driver.
205      *
206      * @return the classpath
207      */
208     public Path createClasspath()
209     {
210         if (this.classpath == null)
211         {
212             this.classpath = new Path(project);
213         }
214         return this.classpath.createPath();
215     }
216 
217     /***
218      * Set the classpath for loading the driver using the classpath reference.
219      *
220      * @param r reference to the classpath
221      */
222     public void setClasspathRef(Reference r)
223     {
224         createClasspath().setRefid(r);
225     }
226 
227     /***
228      * Set the sql command to execute
229      *
230      * @param sql sql command to execute
231      */
232     public void addText(String sql)
233     {
234         this.sqlCommand += sql;
235     }
236 
237     /***
238      * Set the JDBC driver to be used.
239      *
240      * @param driver driver class name
241      */
242     public void setDriver(String driver)
243     {
244         this.driver = driver;
245     }
246 
247     /***
248      * Set the DB connection url.
249      *
250      * @param url connection url
251      */
252     public void setUrl(String url)
253     {
254         this.url = url;
255     }
256 
257     /***
258      * Set the user name for the DB connection.
259      *
260      * @param userId database user
261      */
262     public void setUserid(String userId)
263     {
264         this.userId = userId;
265     }
266 
267     /***
268      * Set the file encoding to use on the sql files read in
269      *
270      * @param encoding the encoding to use on the files
271      */
272     public void setEncoding(String encoding)
273     {
274         this.encoding = encoding;
275     }
276 
277     /***
278      * Set the password for the DB connection.
279      *
280      * @param password database password
281      */
282     public void setPassword(String password)
283     {
284         this.password = password;
285     }
286 
287     /***
288      * Set the autocommit flag for the DB connection.
289      *
290      * @param autocommit the autocommit flag
291      */
292     public void setAutocommit(boolean autocommit)
293     {
294         this.autocommit = autocommit;
295     }
296 
297     /***
298      * Set the statement delimiter.
299      *
300      * <p>For example, set this to "go" and delimitertype to "ROW" for
301      * Sybase ASE or MS SQL Server.</p>
302      *
303      * @param delimiter
304      */
305     public void setDelimiter(String delimiter)
306     {
307         this.delimiter = delimiter;
308     }
309 
310     /***
311      * Set the Delimiter type for this sql task. The delimiter type takes two
312      * values - normal and row. Normal means that any occurence of the delimiter
313      * terminate the SQL command whereas with row, only a line containing just
314      * the delimiter is recognized as the end of the command.
315      *
316      * @param delimiterType
317      */
318     public void setDelimiterType(DelimiterType delimiterType)
319     {
320         this.delimiterType = delimiterType.getValue();
321     }
322 
323     /***
324      * Set the print flag.
325      *
326      * @param print
327      */
328     public void setPrint(boolean print)
329     {
330         this.print = print;
331     }
332 
333     /***
334      * Set the showheaders flag.
335      *
336      * @param showheaders
337      */
338     public void setShowheaders(boolean showheaders)
339     {
340         this.showheaders = showheaders;
341     }
342 
343     /***
344      * Set the output file.
345      *
346      * @param output
347      */
348     public void setOutput(File output)
349     {
350         this.output = output;
351     }
352 
353     /***
354      * Set the rdbms required
355      *
356      * @param vendor
357      */
358     public void setRdbms(String vendor)
359     {
360         this.rdbms = vendor.toLowerCase();
361     }
362 
363     /***
364      * Set the version required
365      *
366      * @param version
367      */
368     public void setVersion(String version)
369     {
370         this.version = version.toLowerCase();
371     }
372 
373     /***
374      * Set the action to perform onerror
375      *
376      * @param action
377      */
378     public void setOnerror(OnError action)
379     {
380         this.onError = action.getValue();
381     }
382 
383     /***
384      * Load the sql file and then execute it
385      *
386      * @throws BuildException
387      */
388     public void execute() throws BuildException
389     {
390         sqlCommand = sqlCommand.trim();
391 
392         if (sqldbmap == null || getSqlDbMap().exists() == false)
393         {
394             throw new BuildException("You haven't provided an sqldbmap, or "
395                     + "the one you specified doesn't exist: " + sqldbmap);
396         }
397 
398         if (driver == null)
399         {
400             throw new BuildException("Driver attribute must be set!", location);
401         }
402         if (userId == null)
403         {
404             throw new BuildException("User Id attribute must be set!",
405                     location);
406         }
407         if (password == null)
408         {
409             throw new BuildException("Password attribute must be set!",
410                     location);
411         }
412         if (url == null)
413         {
414             throw new BuildException("Url attribute must be set!", location);
415         }
416 
417         Properties map = new Properties();
418 
419         try
420         {
421             FileInputStream fis = new FileInputStream(getSqlDbMap());
422             map.load(fis);
423             fis.close();
424         }
425         catch (IOException ioe)
426         {
427             throw new BuildException("Cannot open and process the sqldbmap!");
428         }
429 
430         Map databases = new HashMap();
431 
432         Iterator eachFileName = map.keySet().iterator();
433         while (eachFileName.hasNext())
434         {
435             String sqlfile = (String) eachFileName.next();
436             String database = map.getProperty(sqlfile);
437 
438             List files = (List) databases.get(database);
439 
440             if (files == null)
441             {
442                 files = new ArrayList();
443                 databases.put(database, files);
444             }
445 
446             // We want to make sure that the base schemas
447             // are inserted first.
448             if (sqlfile.indexOf("schema.sql") != -1)
449             {
450                 files.add(0, sqlfile);
451             }
452             else
453             {
454                 files.add(sqlfile);
455             }
456         }
457 
458         Iterator eachDatabase = databases.keySet().iterator();
459         while (eachDatabase.hasNext())
460         {
461             String db = (String) eachDatabase.next();
462             List transactions = new ArrayList();
463             eachFileName = ((List) databases.get(db)).iterator();
464             while (eachFileName.hasNext())
465             {
466                 String fileName = (String) eachFileName.next();
467                 File file = new File(srcDir, fileName);
468 
469                 if (file.exists())
470                 {
471                     Transaction transaction = new Transaction();
472                     transaction.setSrc(file);
473                     transactions.add(transaction);
474                 }
475                 else
476                 {
477                     super.log("File '" + fileName
478                             + "' in sqldbmap does not exist, so skipping it.");
479                 }
480             }
481 
482             insertDatabaseSqlFiles(url, db, transactions);
483         }
484     }
485 
486     /***
487      * Take the base url, the target database and insert a set of SQL
488      * files into the target database.
489      *
490      * @param url
491      * @param database
492      * @param transactions
493      */
494     private void insertDatabaseSqlFiles(String url, String database,
495             List transactions)
496     {
497         url = StringUtils.replace(url, "@DB@", database);
498         System.out.println("Our new url -> " + url);
499 
500         Driver driverInstance = null;
501         try
502         {
503             Class dc;
504             if (classpath != null)
505             {
506                 log("Loading " + driver
507                         + " using AntClassLoader with classpath " + classpath,
508                         Project.MSG_VERBOSE);
509 
510                 loader = new AntClassLoader(project, classpath);
511                 dc = loader.loadClass(driver);
512             }
513             else
514             {
515                 log("Loading " + driver + " using system loader.",
516                         Project.MSG_VERBOSE);
517                 dc = Class.forName(driver);
518             }
519             driverInstance = (Driver) dc.newInstance();
520         }
521         catch (ClassNotFoundException e)
522         {
523             throw new BuildException("Class Not Found: JDBC driver " + driver
524                     + " could not be loaded", location);
525         }
526         catch (IllegalAccessException e)
527         {
528             throw new BuildException("Illegal Access: JDBC driver " + driver
529                     + " could not be loaded", location);
530         }
531         catch (InstantiationException e)
532         {
533             throw new BuildException("Instantiation Exception: JDBC driver "
534                     + driver + " could not be loaded", location);
535         }
536 
537         try
538         {
539             log("connecting to " + url, Project.MSG_VERBOSE);
540             Properties info = new Properties();
541             info.put("user", userId);
542             info.put("password", password);
543             conn = driverInstance.connect(url, info);
544 
545             if (conn == null)
546             {
547                 // Driver doesn't understand the URL
548                 throw new SQLException("No suitable Driver for " + url);
549             }
550 
551             if (!isValidRdbms(conn))
552             {
553                 return;
554             }
555 
556             conn.setAutoCommit(autocommit);
557             statement = conn.createStatement();
558             PrintStream out = System.out;
559             try
560             {
561                 if (output != null)
562                 {
563                     log("Opening PrintStream to output file " + output,
564                             Project.MSG_VERBOSE);
565                     out = new PrintStream(new BufferedOutputStream(
566                             new FileOutputStream(output)));
567                 }
568 
569                 // Process all transactions
570                 for (Iterator it = transactions.iterator(); it.hasNext();)
571                 {
572                     ((Transaction) it.next()).runTransaction(out);
573                     if (!autocommit)
574                     {
575                         log("Commiting transaction", Project.MSG_VERBOSE);
576                         conn.commit();
577                     }
578                 }
579             }
580             finally
581             {
582                 if (out != null && out != System.out)
583                 {
584                     out.close();
585                 }
586             }
587         }
588         catch (IOException e)
589         {
590             if (!autocommit && conn != null && onError.equals("abort"))
591             {
592                 try
593                 {
594                     conn.rollback();
595                 }
596                 catch (SQLException ex)
597                 {
598                     // do nothing.
599                 }
600             }
601             throw new BuildException(e, location);
602         }
603         catch (SQLException e)
604         {
605             if (!autocommit && conn != null && onError.equals("abort"))
606             {
607                 try
608                 {
609                     conn.rollback();
610                 }
611                 catch (SQLException ex)
612                 {
613                     // do nothing.
614                 }
615             }
616             throw new BuildException(e, location);
617         }
618         finally
619         {
620             try
621             {
622                 if (statement != null)
623                 {
624                     statement.close();
625                 }
626                 if (conn != null)
627                 {
628                     conn.close();
629                 }
630             }
631             catch (SQLException e)
632             {
633             }
634         }
635 
636         log(goodSql + " of " + totalSql
637                 + " SQL statements executed successfully");
638     }
639 
640     /***
641      * Read the statements from the .sql file and execute them.
642      * Lines starting with '//', '--' or 'REM ' are ignored.
643      *
644      * @param reader
645      * @param out
646      * @throws SQLException
647      * @throws IOException
648      */
649     protected void runStatements(Reader reader, PrintStream out)
650             throws SQLException, IOException
651     {
652         String sql = "";
653         String line = "";
654 
655         BufferedReader in = new BufferedReader(reader);
656 
657         try
658         {
659             while ((line = in.readLine()) != null)
660             {
661                 line = line.trim();
662                 line = ProjectHelper.replaceProperties(project, line,
663                         project.getProperties());
664                 if (line.startsWith("//") || line.startsWith("--"))
665                 {
666                     continue;
667                 }
668                 if (line.length() > 4
669                         && line.substring(0, 4).equalsIgnoreCase("REM "))
670                 {
671                     continue;
672                 }
673 
674                 sql += " " + line;
675                 sql = sql.trim();
676 
677                 // SQL defines "--" as a comment to EOL
678                 // and in Oracle it may contain a hint
679                 // so we cannot just remove it, instead we must end it
680                 if (line.indexOf("--") >= 0)
681                 {
682                     sql += "\n";
683                 }
684 
685                 if (delimiterType.equals(DelimiterType.NORMAL)
686                         && sql.endsWith(delimiter)
687                         || delimiterType.equals(DelimiterType.ROW)
688                         && line.equals(delimiter))
689                 {
690                     log("SQL: " + sql, Project.MSG_VERBOSE);
691                     execSQL(sql.substring(0, sql.length() - delimiter.length()),
692                             out);
693                     sql = "";
694                 }
695             }
696 
697             // Catch any statements not followed by ;
698             if (!sql.equals(""))
699             {
700                 execSQL(sql, out);
701             }
702         }
703         catch (SQLException e)
704         {
705             throw e;
706         }
707     }
708 
709     /***
710      * Verify if connected to the correct RDBMS
711      *
712      * @param conn
713      */
714     protected boolean isValidRdbms(Connection conn)
715     {
716         if (rdbms == null && version == null)
717         {
718             return true;
719         }
720 
721         try
722         {
723             DatabaseMetaData dmd = conn.getMetaData();
724 
725             if (rdbms != null)
726             {
727                 String theVendor = dmd.getDatabaseProductName().toLowerCase();
728 
729                 log("RDBMS = " + theVendor, Project.MSG_VERBOSE);
730                 if (theVendor == null || theVendor.indexOf(rdbms) < 0)
731                 {
732                     log("Not the required RDBMS: "
733                             + rdbms, Project.MSG_VERBOSE);
734                     return false;
735                 }
736             }
737 
738             if (version != null)
739             {
740                 String theVersion = dmd.getDatabaseProductVersion()
741                         .toLowerCase();
742 
743                 log("Version = " + theVersion, Project.MSG_VERBOSE);
744                 if (theVersion == null || !(theVersion.startsWith(version)
745                         || theVersion.indexOf(" " + version) >= 0))
746                 {
747                     log("Not the required version: \"" + version + "\"",
748                             Project.MSG_VERBOSE);
749                     return false;
750                 }
751             }
752         }
753         catch (SQLException e)
754         {
755             // Could not get the required information
756             log("Failed to obtain required RDBMS information", Project.MSG_ERR);
757             return false;
758         }
759 
760         return true;
761     }
762 
763     /***
764      * Exec the sql statement.
765      *
766      * @param sql
767      * @param out
768      * @throws SQLException
769      */
770     protected void execSQL(String sql, PrintStream out) throws SQLException
771     {
772         // Check and ignore empty statements
773         if ("".equals(sql.trim()))
774         {
775             return;
776         }
777 
778         try
779         {
780             totalSql++;
781             if (!statement.execute(sql))
782             {
783                 log(statement.getUpdateCount() + " rows affected",
784                         Project.MSG_VERBOSE);
785             }
786             else
787             {
788                 if (print)
789                 {
790                     printResults(out);
791                 }
792             }
793 
794             SQLWarning warning = conn.getWarnings();
795             while (warning != null)
796             {
797                 log(warning + " sql warning", Project.MSG_VERBOSE);
798                 warning = warning.getNextWarning();
799             }
800             conn.clearWarnings();
801             goodSql++;
802         }
803         catch (SQLException e)
804         {
805             log("Failed to execute: " + sql, Project.MSG_ERR);
806             if (!onError.equals("continue"))
807             {
808                 throw e;
809             }
810             log(e.toString(), Project.MSG_ERR);
811         }
812     }
813 
814     /***
815      * print any results in the statement.
816      *
817      * @param out
818      * @throws SQLException
819      */
820     protected void printResults(PrintStream out) throws java.sql.SQLException
821     {
822         ResultSet rs = null;
823         do
824         {
825             rs = statement.getResultSet();
826             if (rs != null)
827             {
828                 log("Processing new result set.", Project.MSG_VERBOSE);
829                 ResultSetMetaData md = rs.getMetaData();
830                 int columnCount = md.getColumnCount();
831                 StringBuffer line = new StringBuffer();
832                 if (showheaders)
833                 {
834                     for (int col = 1; col < columnCount; col++)
835                     {
836                          line.append(md.getColumnName(col));
837                          line.append(",");
838                     }
839                     line.append(md.getColumnName(columnCount));
840                     out.println(line);
841                     line.setLength(0);
842                 }
843                 while (rs.next())
844                 {
845                     boolean first = true;
846                     for (int col = 1; col <= columnCount; col++)
847                     {
848                         String columnValue = rs.getString(col);
849                         if (columnValue != null)
850                         {
851                             columnValue = columnValue.trim();
852                         }
853 
854                         if (first)
855                         {
856                             first = false;
857                         }
858                         else
859                         {
860                             line.append(",");
861                         }
862                         line.append(columnValue);
863                     }
864                     out.println(line);
865                     line.setLength(0);
866                 }
867             }
868         }
869         while (statement.getMoreResults());
870         out.println();
871     }
872 
873     /***
874      * Enumerated attribute with the values "continue", "stop" and "abort"
875      * for the onerror attribute.
876      */
877     public static class OnError extends EnumeratedAttribute
878     {
879         public String[] getValues()
880         {
881             return new String[] {"continue", "stop", "abort"};
882         }
883     }
884 
885     /***
886      * Contains the definition of a new transaction element.
887      * Transactions allow several files or blocks of statements
888      * to be executed using the same JDBC connection and commit
889      * operation in between.
890      */
891     public class Transaction
892     {
893         private File tSrcFile = null;
894         private String tSqlCommand = "";
895 
896         public void setSrc(File src)
897         {
898             this.tSrcFile = src;
899         }
900 
901         public void addText(String sql)
902         {
903             this.tSqlCommand += sql;
904         }
905 
906         private void runTransaction(PrintStream out)
907             throws IOException, SQLException
908         {
909             if (tSqlCommand.length() != 0)
910             {
911                 log("Executing commands", Project.MSG_INFO);
912                 runStatements(new StringReader(tSqlCommand), out);
913             }
914 
915             if (tSrcFile != null)
916             {
917                 log("Executing file: " + tSrcFile.getAbsolutePath(),
918                     Project.MSG_INFO);
919                 Reader reader = (encoding == null) ? new FileReader(tSrcFile)
920                         : new InputStreamReader(new FileInputStream(tSrcFile),
921                         encoding);
922                 runStatements(reader, out);
923                 reader.close();
924             }
925         }
926     }
927 }