1. /*
  2. * Copyright 2000-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs;
  18. import org.apache.tools.ant.BuildException;
  19. import org.apache.tools.ant.DirectoryScanner;
  20. import org.apache.tools.ant.Project;
  21. import org.apache.tools.ant.types.EnumeratedAttribute;
  22. import org.apache.tools.ant.types.FileSet;
  23. import java.io.File;
  24. import java.io.PrintStream;
  25. import java.io.BufferedOutputStream;
  26. import java.io.FileOutputStream;
  27. import java.io.IOException;
  28. import java.io.Reader;
  29. import java.io.BufferedReader;
  30. import java.io.StringReader;
  31. import java.io.FileReader;
  32. import java.io.InputStreamReader;
  33. import java.io.FileInputStream;
  34. import java.util.Enumeration;
  35. import java.util.StringTokenizer;
  36. import java.util.Vector;
  37. import java.sql.Connection;
  38. import java.sql.Statement;
  39. import java.sql.SQLException;
  40. import java.sql.SQLWarning;
  41. import java.sql.ResultSet;
  42. import java.sql.ResultSetMetaData;
  43. /**
  44. * Executes a series of SQL statements on a database using JDBC.
  45. *
  46. * <p>Statements can
  47. * either be read in from a text file using the <i>src</i> attribute or from
  48. * between the enclosing SQL tags.</p>
  49. *
  50. * <p>Multiple statements can be provided, separated by semicolons (or the
  51. * defined <i>delimiter</i>). Individual lines within the statements can be
  52. * commented using either --, // or REM at the start of the line.</p>
  53. *
  54. * <p>The <i>autocommit</i> attribute specifies whether auto-commit should be
  55. * turned on or off whilst executing the statements. If auto-commit is turned
  56. * on each statement will be executed and committed. If it is turned off the
  57. * statements will all be executed as one transaction.</p>
  58. *
  59. * <p>The <i>onerror</i> attribute specifies how to proceed when an error occurs
  60. * during the execution of one of the statements.
  61. * The possible values are: <b>continue</b> execution, only show the error;
  62. * <b>stop</b> execution and commit transaction;
  63. * and <b>abort</b> execution and transaction and fail task.</p>
  64. *
  65. * @since Ant 1.2
  66. *
  67. * @ant.task name="sql" category="database"
  68. */
  69. public class SQLExec extends JDBCTask {
  70. /**
  71. * delimiters we support, "normal" and "row"
  72. */
  73. public static class DelimiterType extends EnumeratedAttribute {
  74. public static final String NORMAL = "normal";
  75. public static final String ROW = "row";
  76. public String[] getValues() {
  77. return new String[] {NORMAL, ROW};
  78. }
  79. }
  80. private int goodSql = 0;
  81. private int totalSql = 0;
  82. /**
  83. * Database connection
  84. */
  85. private Connection conn = null;
  86. /**
  87. * files to load
  88. */
  89. private Vector filesets = new Vector();
  90. /**
  91. * SQL statement
  92. */
  93. private Statement statement = null;
  94. /**
  95. * SQL input file
  96. */
  97. private File srcFile = null;
  98. /**
  99. * SQL input command
  100. */
  101. private String sqlCommand = "";
  102. /**
  103. * SQL transactions to perform
  104. */
  105. private Vector transactions = new Vector();
  106. /**
  107. * SQL Statement delimiter
  108. */
  109. private String delimiter = ";";
  110. /**
  111. * The delimiter type indicating whether the delimiter will
  112. * only be recognized on a line by itself
  113. */
  114. private String delimiterType = DelimiterType.NORMAL;
  115. /**
  116. * Print SQL results.
  117. */
  118. private boolean print = false;
  119. /**
  120. * Print header columns.
  121. */
  122. private boolean showheaders = true;
  123. /**
  124. * Results Output file.
  125. */
  126. private File output = null;
  127. /**
  128. * Action to perform if an error is found
  129. **/
  130. private String onError = "abort";
  131. /**
  132. * Encoding to use when reading SQL statements from a file
  133. */
  134. private String encoding = null;
  135. /**
  136. * Append to an existing file or overwrite it?
  137. */
  138. private boolean append = false;
  139. /**
  140. * Keep the format of a sql block?
  141. */
  142. private boolean keepformat = false;
  143. /**
  144. * Argument to Statement.setEscapeProcessing
  145. *
  146. * @since Ant 1.6
  147. */
  148. private boolean escapeProcessing = true;
  149. /**
  150. * Set the name of the SQL file to be run.
  151. * Required unless statements are enclosed in the build file
  152. */
  153. public void setSrc(File srcFile) {
  154. this.srcFile = srcFile;
  155. }
  156. /**
  157. * Set an inline SQL command to execute.
  158. * NB: Properties are not expanded in this text.
  159. */
  160. public void addText(String sql) {
  161. this.sqlCommand += sql;
  162. }
  163. /**
  164. * Adds a set of files (nested fileset attribute).
  165. */
  166. public void addFileset(FileSet set) {
  167. filesets.addElement(set);
  168. }
  169. /**
  170. * Add a SQL transaction to execute
  171. */
  172. public Transaction createTransaction() {
  173. Transaction t = new Transaction();
  174. transactions.addElement(t);
  175. return t;
  176. }
  177. /**
  178. * Set the file encoding to use on the SQL files read in
  179. *
  180. * @param encoding the encoding to use on the files
  181. */
  182. public void setEncoding(String encoding) {
  183. this.encoding = encoding;
  184. }
  185. /**
  186. * Set the delimiter that separates SQL statements. Defaults to ";";
  187. * optional
  188. *
  189. * <p>For example, set this to "go" and delimitertype to "ROW" for
  190. * Sybase ASE or MS SQL Server.</p>
  191. */
  192. public void setDelimiter(String delimiter) {
  193. this.delimiter = delimiter;
  194. }
  195. /**
  196. * Set the delimiter type: "normal" or "row" (default "normal").
  197. *
  198. * <p>The delimiter type takes two values - normal and row. Normal
  199. * means that any occurrence of the delimiter terminate the SQL
  200. * command whereas with row, only a line containing just the
  201. * delimiter is recognized as the end of the command.</p>
  202. */
  203. public void setDelimiterType(DelimiterType delimiterType) {
  204. this.delimiterType = delimiterType.getValue();
  205. }
  206. /**
  207. * Print result sets from the statements;
  208. * optional, default false
  209. */
  210. public void setPrint(boolean print) {
  211. this.print = print;
  212. }
  213. /**
  214. * Print headers for result sets from the
  215. * statements; optional, default true.
  216. */
  217. public void setShowheaders(boolean showheaders) {
  218. this.showheaders = showheaders;
  219. }
  220. /**
  221. * Set the output file;
  222. * optional, defaults to the Ant log.
  223. */
  224. public void setOutput(File output) {
  225. this.output = output;
  226. }
  227. /**
  228. * whether output should be appended to or overwrite
  229. * an existing file. Defaults to false.
  230. *
  231. * @since Ant 1.5
  232. */
  233. public void setAppend(boolean append) {
  234. this.append = append;
  235. }
  236. /**
  237. * Action to perform when statement fails: continue, stop, or abort
  238. * optional; default "abort"
  239. */
  240. public void setOnerror(OnError action) {
  241. this.onError = action.getValue();
  242. }
  243. /**
  244. * whether or not format should be preserved.
  245. * Defaults to false.
  246. *
  247. * @param keepformat The keepformat to set
  248. */
  249. public void setKeepformat(boolean keepformat) {
  250. this.keepformat = keepformat;
  251. }
  252. /**
  253. * Set escape processing for statements.
  254. *
  255. * @since Ant 1.6
  256. */
  257. public void setEscapeProcessing(boolean enable) {
  258. escapeProcessing = enable;
  259. }
  260. /**
  261. * Load the sql file and then execute it
  262. */
  263. public void execute() throws BuildException {
  264. Vector savedTransaction = (Vector) transactions.clone();
  265. String savedSqlCommand = sqlCommand;
  266. sqlCommand = sqlCommand.trim();
  267. try {
  268. if (srcFile == null && sqlCommand.length() == 0
  269. && filesets.isEmpty()) {
  270. if (transactions.size() == 0) {
  271. throw new BuildException("Source file or fileset, "
  272. + "transactions or sql statement "
  273. + "must be set!", getLocation());
  274. }
  275. }
  276. if (srcFile != null && !srcFile.exists()) {
  277. throw new BuildException("Source file does not exist!", getLocation());
  278. }
  279. // deal with the filesets
  280. for (int i = 0; i < filesets.size(); i++) {
  281. FileSet fs = (FileSet) filesets.elementAt(i);
  282. DirectoryScanner ds = fs.getDirectoryScanner(getProject());
  283. File srcDir = fs.getDir(getProject());
  284. String[] srcFiles = ds.getIncludedFiles();
  285. // Make a transaction for each file
  286. for (int j = 0; j < srcFiles.length; j++) {
  287. Transaction t = createTransaction();
  288. t.setSrc(new File(srcDir, srcFiles[j]));
  289. }
  290. }
  291. // Make a transaction group for the outer command
  292. Transaction t = createTransaction();
  293. t.setSrc(srcFile);
  294. t.addText(sqlCommand);
  295. conn = getConnection();
  296. if (!isValidRdbms(conn)) {
  297. return;
  298. }
  299. try {
  300. statement = conn.createStatement();
  301. statement.setEscapeProcessing(escapeProcessing);
  302. PrintStream out = System.out;
  303. try {
  304. if (output != null) {
  305. log("Opening PrintStream to output file " + output,
  306. Project.MSG_VERBOSE);
  307. out = new PrintStream(
  308. new BufferedOutputStream(
  309. new FileOutputStream(output
  310. .getAbsolutePath(),
  311. append)));
  312. }
  313. // Process all transactions
  314. for (Enumeration e = transactions.elements();
  315. e.hasMoreElements();) {
  316. ((Transaction) e.nextElement()).runTransaction(out);
  317. if (!isAutocommit()) {
  318. log("Committing transaction", Project.MSG_VERBOSE);
  319. conn.commit();
  320. }
  321. }
  322. } finally {
  323. if (out != null && out != System.out) {
  324. out.close();
  325. }
  326. }
  327. } catch (IOException e) {
  328. if (!isAutocommit() && conn != null && onError.equals("abort")) {
  329. try {
  330. conn.rollback();
  331. } catch (SQLException ex) {
  332. // ignore
  333. }
  334. }
  335. throw new BuildException(e, getLocation());
  336. } catch (SQLException e) {
  337. if (!isAutocommit() && conn != null && onError.equals("abort")) {
  338. try {
  339. conn.rollback();
  340. } catch (SQLException ex) {
  341. // ignore
  342. }
  343. }
  344. throw new BuildException(e, getLocation());
  345. } finally {
  346. try {
  347. if (statement != null) {
  348. statement.close();
  349. }
  350. if (conn != null) {
  351. conn.close();
  352. }
  353. } catch (SQLException ex) {
  354. // ignore
  355. }
  356. }
  357. log(goodSql + " of " + totalSql
  358. + " SQL statements executed successfully");
  359. } finally {
  360. transactions = savedTransaction;
  361. sqlCommand = savedSqlCommand;
  362. }
  363. }
  364. /**
  365. * read in lines and execute them
  366. */
  367. protected void runStatements(Reader reader, PrintStream out)
  368. throws SQLException, IOException {
  369. StringBuffer sql = new StringBuffer();
  370. String line = "";
  371. BufferedReader in = new BufferedReader(reader);
  372. while ((line = in.readLine()) != null) {
  373. if (!keepformat) {
  374. line = line.trim();
  375. }
  376. line = getProject().replaceProperties(line);
  377. if (!keepformat) {
  378. if (line.startsWith("//")) {
  379. continue;
  380. }
  381. if (line.startsWith("--")) {
  382. continue;
  383. }
  384. StringTokenizer st = new StringTokenizer(line);
  385. if (st.hasMoreTokens()) {
  386. String token = st.nextToken();
  387. if ("REM".equalsIgnoreCase(token)) {
  388. continue;
  389. }
  390. }
  391. }
  392. if (!keepformat) {
  393. sql.append(" " + line);
  394. } else {
  395. sql.append("\n" + line);
  396. }
  397. // SQL defines "--" as a comment to EOL
  398. // and in Oracle it may contain a hint
  399. // so we cannot just remove it, instead we must end it
  400. if (!keepformat) {
  401. if (line.indexOf("--") >= 0) {
  402. sql.append("\n");
  403. }
  404. }
  405. if ((delimiterType.equals(DelimiterType.NORMAL)
  406. && sql.toString().endsWith(delimiter))
  407. ||
  408. (delimiterType.equals(DelimiterType.ROW)
  409. && line.equals(delimiter))) {
  410. execSQL(sql.substring(0, sql.length() - delimiter.length()),
  411. out);
  412. sql.replace(0, sql.length(), "");
  413. }
  414. }
  415. // Catch any statements not followed by ;
  416. if (!sql.equals("")) {
  417. execSQL(sql.toString(), out);
  418. }
  419. }
  420. /**
  421. * Exec the sql statement.
  422. */
  423. protected void execSQL(String sql, PrintStream out) throws SQLException {
  424. // Check and ignore empty statements
  425. if ("".equals(sql.trim())) {
  426. return;
  427. }
  428. try {
  429. totalSql++;
  430. log("SQL: " + sql, Project.MSG_VERBOSE);
  431. boolean ret;
  432. int updateCount = 0, updateCountTotal = 0;
  433. ResultSet resultSet = null;
  434. ret = statement.execute(sql);
  435. updateCount = statement.getUpdateCount();
  436. resultSet = statement.getResultSet();
  437. do {
  438. if (!ret) {
  439. if (updateCount != -1) {
  440. updateCountTotal += updateCount;
  441. }
  442. } else {
  443. if (print) {
  444. printResults(out);
  445. }
  446. }
  447. ret = statement.getMoreResults();
  448. updateCount = statement.getUpdateCount();
  449. resultSet = statement.getResultSet();
  450. } while (ret);
  451. log(updateCountTotal + " rows affected",
  452. Project.MSG_VERBOSE);
  453. if (print) {
  454. StringBuffer line = new StringBuffer();
  455. line.append(updateCountTotal + " rows affected");
  456. out.println(line);
  457. }
  458. SQLWarning warning = conn.getWarnings();
  459. while (warning != null) {
  460. log(warning + " sql warning", Project.MSG_VERBOSE);
  461. warning = warning.getNextWarning();
  462. }
  463. conn.clearWarnings();
  464. goodSql++;
  465. } catch (SQLException e) {
  466. log("Failed to execute: " + sql, Project.MSG_ERR);
  467. if (!onError.equals("continue")) {
  468. throw e;
  469. }
  470. log(e.toString(), Project.MSG_ERR);
  471. }
  472. }
  473. /**
  474. * print any results in the statement.
  475. */
  476. protected void printResults(PrintStream out) throws java.sql.SQLException {
  477. ResultSet rs = null;
  478. rs = statement.getResultSet();
  479. if (rs != null) {
  480. log("Processing new result set.", Project.MSG_VERBOSE);
  481. ResultSetMetaData md = rs.getMetaData();
  482. int columnCount = md.getColumnCount();
  483. StringBuffer line = new StringBuffer();
  484. if (showheaders) {
  485. for (int col = 1; col < columnCount; col++) {
  486. line.append(md.getColumnName(col));
  487. line.append(",");
  488. }
  489. line.append(md.getColumnName(columnCount));
  490. out.println(line);
  491. line = new StringBuffer();
  492. }
  493. while (rs.next()) {
  494. boolean first = true;
  495. for (int col = 1; col <= columnCount; col++) {
  496. String columnValue = rs.getString(col);
  497. if (columnValue != null) {
  498. columnValue = columnValue.trim();
  499. }
  500. if (first) {
  501. first = false;
  502. } else {
  503. line.append(",");
  504. }
  505. line.append(columnValue);
  506. }
  507. out.println(line);
  508. line = new StringBuffer();
  509. }
  510. }
  511. out.println();
  512. }
  513. /**
  514. * The action a task should perform on an error,
  515. * one of "continue", "stop" and "abort"
  516. */
  517. public static class OnError extends EnumeratedAttribute {
  518. public String[] getValues() {
  519. return new String[] {"continue", "stop", "abort"};
  520. }
  521. }
  522. /**
  523. * Contains the definition of a new transaction element.
  524. * Transactions allow several files or blocks of statements
  525. * to be executed using the same JDBC connection and commit
  526. * operation in between.
  527. */
  528. public class Transaction {
  529. private File tSrcFile = null;
  530. private String tSqlCommand = "";
  531. /**
  532. *
  533. */
  534. public void setSrc(File src) {
  535. this.tSrcFile = src;
  536. }
  537. /**
  538. *
  539. */
  540. public void addText(String sql) {
  541. this.tSqlCommand += sql;
  542. }
  543. /**
  544. *
  545. */
  546. private void runTransaction(PrintStream out)
  547. throws IOException, SQLException {
  548. if (tSqlCommand.length() != 0) {
  549. log("Executing commands", Project.MSG_INFO);
  550. runStatements(new StringReader(tSqlCommand), out);
  551. }
  552. if (tSrcFile != null) {
  553. log("Executing file: " + tSrcFile.getAbsolutePath(),
  554. Project.MSG_INFO);
  555. Reader reader =
  556. (encoding == null) ? new FileReader(tSrcFile)
  557. : new InputStreamReader(
  558. new FileInputStream(tSrcFile),
  559. encoding);
  560. try {
  561. runStatements(reader, out);
  562. } finally {
  563. reader.close();
  564. }
  565. }
  566. }
  567. }
  568. }