/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; import java.io.StringReader; import java.nio.charset.Charset; import java.sql.Blob; import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.sql.Types; import java.util.List; import java.util.Locale; import java.util.StringTokenizer; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.resources.Appendable; import org.apache.tools.ant.types.resources.FileProvider; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.types.resources.Union; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.KeepAliveOutputStream; import org.apache.tools.ant.util.StringUtils; /** * Executes a series of SQL statements on a database using JDBC. * * <p>Statements can * either be read in from a text file using the <i>src</i> attribute or from * between the enclosing SQL tags.</p> * * <p>Multiple statements can be provided, separated by semicolons (or the * defined <i>delimiter</i>). Individual lines within the statements can be * commented using either --, // or REM at the start of the line.</p> * * <p>The <i>autocommit</i> attribute specifies whether auto-commit should be * turned on or off whilst executing the statements. If auto-commit is turned * on each statement will be executed and committed. If it is turned off the * statements will all be executed as one transaction.</p> * * <p>The <i>onerror</i> attribute specifies how to proceed when an error occurs * during the execution of one of the statements. * The possible values are: <b>continue</b> execution, only show the error; * <b>stop</b> execution and commit transaction; * and <b>abort</b> execution and transaction and fail task.</p> * * @since Ant 1.2 * * @ant.task name="sql" category="database" */ public class SQLExec extends JDBCTask { /** * delimiters we support, "normal" and "row" */ public static class DelimiterType extends EnumeratedAttribute { /** The enumerated strings */ public static final String NORMAL = "normal", ROW = "row"; /** @return the enumerated strings */ @Override public String[] getValues() { return new String[] { NORMAL, ROW }; } } private int goodSql = 0; private int totalSql = 0; /** * Database connection */ private Connection conn = null; /** * files to load */ private Union resources; /** * SQL statement */ private Statement statement = null; /** * SQL input file */ private File srcFile = null; /** * SQL input command */ private String sqlCommand = ""; /** * SQL transactions to perform */ private List<Transaction> transactions = new Vector<>(); /** * SQL Statement delimiter */ private String delimiter = ";"; /** * The delimiter type indicating whether the delimiter will * only be recognized on a line by itself */ private String delimiterType = DelimiterType.NORMAL; /** * Print SQL results. */ private boolean print = false; /** * Print header columns. */ private boolean showheaders = true; /** * Print SQL stats (rows affected) */ private boolean showtrailers = true; /** * Results Output Resource. */ private Resource output = null; /** * Output encoding. */ private String outputEncoding = null; /** * Action to perform if an error is found */ private String onError = "abort"; /** * Encoding to use when reading SQL statements from a file */ private String encoding = null; /** * Append to an existing file or overwrite it? */ private boolean append = false; /** * Keep the format of a sql block? */ private boolean keepformat = false; /** * Argument to Statement.setEscapeProcessing * * @since Ant 1.6 */ private boolean escapeProcessing = true; /** * should properties be expanded in text? * false for backwards compatibility * * @since Ant 1.7 */ private boolean expandProperties = true; /** * should we print raw BLOB data? * @since Ant 1.7.1 */ private boolean rawBlobs; /** * delimiters must match in case and whitespace is significant. * @since Ant 1.8.0 */ private boolean strictDelimiterMatching = true; /** * whether to show SQLWarnings as WARN messages. * @since Ant 1.8.0 */ private boolean showWarnings = false; /** * The column separator used when printing the results. * * <p>Defaults to ","</p> * * @since Ant 1.8.0 */ private String csvColumnSep = ","; /** * The character used to quote column values. * * <p>If set, columns that contain either the column separator or * the quote character itself will be surrounded by the quote * character. The quote character itself will be doubled if it * appears inside of the column's value.</p> * * <p>If this value is not set (the default), no column values * will be quoted, not even if they contain the column * separator.</p> * * <p><b>Note:<b> BLOB values will never be quoted.</p> * * <p>Defaults to "not set"</p> * * @since Ant 1.8.0 */ private String csvQuoteChar = null; /** * Whether a warning is an error - in which case onError applies. * @since Ant 1.8.0 */ private boolean treatWarningsAsErrors = false; /** * The name of the property to set in the event of an error * @since Ant 1.8.0 */ private String errorProperty = null; /** * The name of the property to set in the event of a warning * @since Ant 1.8.0 */ private String warningProperty = null; /** * The name of the property that receives the number of rows * returned * @since Ant 1.8.0 */ private String rowCountProperty = null; /** * The name of the property to force the csv quote character */ private boolean forceCsvQuoteChar = false; /** * Set the name of the SQL file to be run. * Required unless statements are enclosed in the build file * @param srcFile the file containing the SQL command. */ public void setSrc(File srcFile) { this.srcFile = srcFile; } /** * Enable property expansion inside nested text * * @param expandProperties if true expand properties. * @since Ant 1.7 */ public void setExpandProperties(boolean expandProperties) { this.expandProperties = expandProperties; } /** * is property expansion inside inline text enabled? * * @return true if properties are to be expanded. * @since Ant 1.7 */ public boolean getExpandProperties() { return expandProperties; } /** * Set an inline SQL command to execute. * NB: Properties are not expanded in this text unless {@link #expandProperties} * is set. * @param sql an inline string containing the SQL command. */ public void addText(String sql) { //there is no need to expand properties here as that happens when Transaction.addText is //called; to do so here would be an error. this.sqlCommand += sql; } /** * Adds a set of files (nested fileset attribute). * @param set a set of files contains SQL commands, each File is run in * a separate transaction. */ public void addFileset(FileSet set) { add(set); } /** * Adds a collection of resources (nested element). * @param rc a collection of resources containing SQL commands, * each resource is run in a separate transaction. * @since Ant 1.7 */ public void add(ResourceCollection rc) { if (rc == null) { throw new BuildException("Cannot add null ResourceCollection"); } synchronized (this) { if (resources == null) { resources = new Union(); } } resources.add(rc); } /** * Add a SQL transaction to execute * @return a Transaction to be configured. */ public Transaction createTransaction() { Transaction t = new Transaction(); transactions.add(t); return t; } /** * Set the file encoding to use on the SQL files read in * * @param encoding the encoding to use on the files */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * Set the delimiter that separates SQL statements. Defaults to ";"; * optional * * <p>For example, set this to "go" and delimitertype to "ROW" for * Sybase ASE or MS SQL Server.</p> * @param delimiter the separator. */ public void setDelimiter(String delimiter) { this.delimiter = delimiter; } /** * Set the delimiter type: "normal" or "row" (default "normal"). * * <p>The delimiter type takes two values - normal and row. Normal * means that any occurrence of the delimiter terminate the SQL * command whereas with row, only a line containing just the * delimiter is recognized as the end of the command.</p> * @param delimiterType the type of delimiter - "normal" or "row". */ public void setDelimiterType(DelimiterType delimiterType) { this.delimiterType = delimiterType.getValue(); } /** * Print result sets from the statements; * optional, default false * @param print if true print result sets. */ public void setPrint(boolean print) { this.print = print; } /** * Print headers for result sets from the * statements; optional, default true. * @param showheaders if true print headers of result sets. */ public void setShowheaders(boolean showheaders) { this.showheaders = showheaders; } /** * Print trailing info (rows affected) for the SQL * Addresses Bug/Request #27446 * @param showtrailers if true prints the SQL rows affected * @since Ant 1.7 */ public void setShowtrailers(boolean showtrailers) { this.showtrailers = showtrailers; } /** * Set the output file; * optional, defaults to the Ant log. * @param output the output file to use for logging messages. */ public void setOutput(File output) { setOutput(new FileResource(getProject(), output)); } /** * Set the output Resource; * optional, defaults to the Ant log. * @param output the output Resource to store results. * @since Ant 1.8 */ public void setOutput(Resource output) { this.output = output; } /** * The encoding to use when writing the result to a resource. * <p>Default's to the platform's default encoding</p> * @param outputEncoding the name of the encoding or null for the * platform's default encoding * @since Ant 1.9.4 */ public void setOutputEncoding(String outputEncoding) { this.outputEncoding = outputEncoding; } /** * whether output should be appended to or overwrite * an existing file. Defaults to false. * * @since Ant 1.5 * @param append if true append to an existing file. */ public void setAppend(boolean append) { this.append = append; } /** * Action to perform when statement fails: continue, stop, or abort * optional; default "abort" * @param action the action to perform on statement failure. */ public void setOnerror(OnError action) { this.onError = action.getValue(); } /** * whether or not format should be preserved. * Defaults to false. * * @param keepformat The keepformat to set */ public void setKeepformat(boolean keepformat) { this.keepformat = keepformat; } /** * Set escape processing for statements. * @param enable if true enable escape processing, default is true. * @since Ant 1.6 */ public void setEscapeProcessing(boolean enable) { escapeProcessing = enable; } /** * Set whether to print raw BLOBs rather than their string (hex) representations. * @param rawBlobs whether to print raw BLOBs. * @since Ant 1.7.1 */ public void setRawBlobs(boolean rawBlobs) { this.rawBlobs = rawBlobs; } /** * If false, delimiters will be searched for in a case-insensitive * manner (i.e. delimiter="go" matches "GO") and surrounding * whitespace will be ignored (delimiter="go" matches "GO "). * @since Ant 1.8.0 */ public void setStrictDelimiterMatching(boolean b) { strictDelimiterMatching = b; } /** * whether to show SQLWarnings as WARN messages. * @since Ant 1.8.0 */ public void setShowWarnings(boolean b) { showWarnings = b; } /** * Whether a warning is an error - in which case onError applies. * @since Ant 1.8.0 */ public void setTreatWarningsAsErrors(boolean b) { treatWarningsAsErrors = b; } /** * The column separator used when printing the results. * * <p>Defaults to ","</p> * * @since Ant 1.8.0 */ public void setCsvColumnSeparator(String s) { csvColumnSep = s; } /** * The character used to quote column values. * * <p>If set, columns that contain either the column separator or * the quote character itself will be surrounded by the quote * character. The quote character itself will be doubled if it * appears inside of the column's value.</p> * * <p>If this value is not set (the default), no column values * will be quoted, not even if they contain the column * separator.</p> * * <p><b>Note:</b> BLOB values will never be quoted.</p> * * <p>Defaults to "not set"</p> * * @since Ant 1.8.0 */ public void setCsvQuoteCharacter(String s) { if (s != null && s.length() > 1) { throw new BuildException( "The quote character must be a single character."); } csvQuoteChar = s; } /** * Property to set to "true" if a statement throws an error. * * @param errorProperty the name of the property to set in the * event of an error. * @since Ant 1.8.0 */ public void setErrorProperty(String errorProperty) { this.errorProperty = errorProperty; } /** * Property to set to "true" if a statement produces a warning. * * @param warningProperty the name of the property to set in the * event of a warning. * @since Ant 1.8.0 */ public void setWarningProperty(String warningProperty) { this.warningProperty = warningProperty; } /** * Sets a given property to the number of rows in the first * statement that returned a row count. * @since Ant 1.8.0 */ public void setRowCountProperty(String rowCountProperty) { this.rowCountProperty = rowCountProperty; } /** * Force the csv quote character */ public void setForceCsvQuoteChar(boolean forceCsvQuoteChar) { this.forceCsvQuoteChar = forceCsvQuoteChar; } /** * Load the sql file and then execute it * @throws BuildException on error. */ @Override public void execute() throws BuildException { List<Transaction> savedTransaction = new Vector<>(transactions); String savedSqlCommand = sqlCommand; sqlCommand = sqlCommand.trim(); try { if (srcFile == null && sqlCommand.isEmpty() && resources == null) { if (transactions.isEmpty()) { throw new BuildException( "Source file or resource collection, transactions or sql statement must be set!", getLocation()); } } if (srcFile != null && !srcFile.isFile()) { throw new BuildException("Source file " + srcFile + " is not a file!", getLocation()); } if (resources != null) { // deal with the resources for (Resource r : resources) { // Make a transaction for each resource Transaction t = createTransaction(); t.setSrcResource(r); } } // Make a transaction group for the outer command Transaction t = createTransaction(); t.setSrc(srcFile); t.addText(sqlCommand); if (getConnection() == null) { // not a valid rdbms return; } try { PrintStream out = KeepAliveOutputStream.wrapSystemOut(); try { if (output != null) { log("Opening PrintStream to output Resource " + output, Project.MSG_VERBOSE); OutputStream os = null; FileProvider fp = output.as(FileProvider.class); if (fp != null) { os = FileUtils.newOutputStream(fp.getFile().toPath(), append); } else { if (append) { Appendable a = output.as(Appendable.class); if (a != null) { os = a.getAppendOutputStream(); } } if (os == null) { os = output.getOutputStream(); if (append) { log("Ignoring append=true for non-appendable" + " resource " + output, Project.MSG_WARN); } } } if (outputEncoding != null) { out = new PrintStream(new BufferedOutputStream(os), false, outputEncoding); } else { out = new PrintStream(new BufferedOutputStream(os)); } } // Process all transactions for (Transaction txn : transactions) { txn.runTransaction(out); if (!isAutocommit()) { log("Committing transaction", Project.MSG_VERBOSE); getConnection().commit(); } } } finally { FileUtils.close(out); } } catch (IOException | SQLException e) { closeQuietly(); setErrorProperty(); if ("abort".equals(onError)) { throw new BuildException(e, getLocation()); } } finally { try { FileUtils.close(getStatement()); } catch (SQLException ex) { // ignore } FileUtils.close(getConnection()); } log(goodSql + " of " + totalSql + " SQL statements executed successfully"); } finally { transactions = savedTransaction; sqlCommand = savedSqlCommand; } } /** * read in lines and execute them * @param reader the reader contains sql lines. * @param out the place to output results. * @throws SQLException on sql problems * @throws IOException on io problems */ protected void runStatements(Reader reader, PrintStream out) throws SQLException, IOException { StringBuffer sql = new StringBuffer(); BufferedReader in = new BufferedReader(reader); String line; while ((line = in.readLine()) != null) { if (!keepformat) { line = line.trim(); } if (expandProperties) { line = getProject().replaceProperties(line); } if (!keepformat) { if (line.startsWith("//")) { continue; } if (line.startsWith("--")) { continue; } StringTokenizer st = new StringTokenizer(line); if (st.hasMoreTokens()) { String token = st.nextToken(); if ("REM".equalsIgnoreCase(token)) { continue; } } } sql.append(keepformat ? "\n" : " ").append(line); // SQL defines "--" as a comment to EOL // and in Oracle it may contain a hint // so we cannot just remove it, instead we must end it if (!keepformat && line.indexOf("--") >= 0) { sql.append("\n"); } int lastDelimPos = lastDelimiterPosition(sql, line); if (lastDelimPos > -1) { execSQL(sql.substring(0, lastDelimPos), out); sql.replace(0, sql.length(), ""); } } // Catch any statements not followed by ; if (sql.length() > 0) { execSQL(sql.toString(), out); } } /** * Exec the sql statement. * @param sql the SQL statement to execute * @param out the place to put output * @throws SQLException on SQL problems */ protected void execSQL(String sql, PrintStream out) throws SQLException { // Check and ignore empty statements if (sql.trim().isEmpty()) { return; } ResultSet resultSet = null; try { totalSql++; log("SQL: " + sql, Project.MSG_VERBOSE); boolean ret; int updateCount = 0, updateCountTotal = 0; ret = getStatement().execute(sql); updateCount = getStatement().getUpdateCount(); do { if (updateCount != -1) { updateCountTotal += updateCount; } if (ret) { resultSet = getStatement().getResultSet(); printWarnings(resultSet.getWarnings(), false); resultSet.clearWarnings(); if (print) { printResults(resultSet, out); } } ret = getStatement().getMoreResults(); updateCount = getStatement().getUpdateCount(); } while (ret || updateCount != -1); printWarnings(getStatement().getWarnings(), false); getStatement().clearWarnings(); log(updateCountTotal + " rows affected", Project.MSG_VERBOSE); if (updateCountTotal != -1) { setRowCountProperty(updateCountTotal); } if (print && showtrailers) { out.println(updateCountTotal + " rows affected"); } SQLWarning warning = getConnection().getWarnings(); printWarnings(warning, true); getConnection().clearWarnings(); goodSql++; } catch (SQLException e) { log("Failed to execute: " + sql, Project.MSG_ERR); setErrorProperty(); if (!"abort".equals(onError)) { log(e.toString(), Project.MSG_ERR); } if (!"continue".equals(onError)) { throw e; } } finally { FileUtils.close(resultSet); } } /** * print any results in the statement * @deprecated since 1.6.x. * Use {@link #printResults(java.sql.ResultSet, java.io.PrintStream) * the two arg version} instead. * @param out the place to print results * @throws SQLException on SQL problems. */ @Deprecated protected void printResults(PrintStream out) throws SQLException { try (ResultSet rs = getStatement().getResultSet()) { printResults(rs, out); } } /** * print any results in the result set. * @param rs the resultset to print information about * @param out the place to print results * @throws SQLException on SQL problems. * @since Ant 1.6.3 */ protected void printResults(ResultSet rs, PrintStream out) throws SQLException { if (rs != null) { log("Processing new result set.", Project.MSG_VERBOSE); ResultSetMetaData md = rs.getMetaData(); int columnCount = md.getColumnCount(); if (columnCount > 0) { if (showheaders) { out.print(maybeQuote(md.getColumnName(1))); for (int col = 2; col <= columnCount; col++) { out.print(csvColumnSep); out.print(maybeQuote(md.getColumnName(col))); } out.println(); } while (rs.next()) { printValue(rs, 1, out); for (int col = 2; col <= columnCount; col++) { out.print(csvColumnSep); printValue(rs, col, out); } out.println(); printWarnings(rs.getWarnings(), false); } } } out.println(); } private void printValue(ResultSet rs, int col, PrintStream out) throws SQLException { if (rawBlobs && rs.getMetaData().getColumnType(col) == Types.BLOB) { Blob blob = rs.getBlob(col); if (blob != null) { new StreamPumper(rs.getBlob(col).getBinaryStream(), out).run(); } } else { out.print(maybeQuote(rs.getString(col))); } } private String maybeQuote(String s) { if (csvQuoteChar == null || s == null || (!forceCsvQuoteChar && s.indexOf(csvColumnSep) == -1 && s.indexOf(csvQuoteChar) == -1)) { return s; } StringBuilder sb = new StringBuilder(csvQuoteChar); int len = s.length(); char q = csvQuoteChar.charAt(0); for (int i = 0; i < len; i++) { char c = s.charAt(i); if (c == q) { sb.append(q); } sb.append(c); } return sb.append(csvQuoteChar).toString(); } /* * Closes an unused connection after an error and doesn't rethrow * a possible SQLException * @since Ant 1.7 */ private void closeQuietly() { if (!isAutocommit() && getConnection() != null && "abort".equals(onError)) { try { getConnection().rollback(); } catch (SQLException ex) { // ignore } } } /** * Caches the connection returned by the base class's getConnection method. * * <p>Subclasses that need to provide a different connection than * the base class would, should override this method but keep in * mind that this class expects to get the same connection * instance on consecutive calls.</p> * * <p>returns null if the connection does not connect to the * expected RDBMS.</p> */ @Override protected Connection getConnection() { if (conn == null) { conn = super.getConnection(); if (!isValidRdbms(conn)) { conn = null; } } return conn; } /** * Creates and configures a Statement instance which is then * cached for subsequent calls. * * <p>Subclasses that want to provide different Statement * instances, should override this method but keep in mind that * this class expects to get the same connection instance on * consecutive calls.</p> */ protected Statement getStatement() throws SQLException { if (statement == null) { statement = getConnection().createStatement(); statement.setEscapeProcessing(escapeProcessing); } return statement; } /** * The action a task should perform on an error, * one of "continue", "stop" and "abort" */ public static class OnError extends EnumeratedAttribute { /** @return the enumerated values */ @Override public String[] getValues() { return new String[] { "continue", "stop", "abort" }; } } /** * Contains the definition of a new transaction element. * Transactions allow several files or blocks of statements * to be executed using the same JDBC connection and commit * operation in between. */ public class Transaction { private Resource tSrcResource = null; private String tSqlCommand = ""; /** * Set the source file attribute. * @param src the source file */ public void setSrc(File src) { //there are places (in this file, and perhaps elsewhere, where it is assumed //that null is an acceptable parameter. if (src != null) { setSrcResource(new FileResource(src)); } } /** * Set the source resource attribute. * @param src the source file * @since Ant 1.7 */ public void setSrcResource(Resource src) { if (tSrcResource != null) { throw new BuildException("only one resource per transaction"); } tSrcResource = src; } /** * Set inline text * @param sql the inline text */ public void addText(String sql) { if (sql != null) { this.tSqlCommand += sql; } } /** * Set the source resource. * @param a the source resource collection. * @since Ant 1.7 */ public void addConfigured(ResourceCollection a) { if (a.size() != 1) { throw new BuildException( "only single argument resource collections are supported."); } setSrcResource(a.iterator().next()); } private void runTransaction(PrintStream out) throws IOException, SQLException { if (!tSqlCommand.isEmpty()) { log("Executing commands", Project.MSG_INFO); runStatements(new StringReader(tSqlCommand), out); } if (tSrcResource != null) { log("Executing resource: " + tSrcResource.toString(), Project.MSG_INFO); Charset charset = encoding == null ? Charset.defaultCharset() : Charset.forName(encoding); try (Reader reader = new InputStreamReader( tSrcResource.getInputStream(), charset)) { runStatements(reader, out); } } } } public int lastDelimiterPosition(StringBuffer buf, String currentLine) { if (strictDelimiterMatching) { if ((delimiterType.equals(DelimiterType.NORMAL) && StringUtils.endsWith(buf, delimiter)) || (delimiterType.equals(DelimiterType.ROW) && currentLine.equals(delimiter))) { return buf.length() - delimiter.length(); } // no match return -1; } String d = delimiter.trim().toLowerCase(Locale.ENGLISH); if (DelimiterType.NORMAL.equals(delimiterType)) { // still trying to avoid wasteful copying, see // StringUtils.endsWith int endIndex = delimiter.length() - 1; int bufferIndex = buf.length() - 1; while (bufferIndex >= 0 && Character.isWhitespace(buf.charAt(bufferIndex))) { --bufferIndex; } if (bufferIndex < endIndex) { return -1; } while (endIndex >= 0) { if (buf.substring(bufferIndex, bufferIndex + 1) .toLowerCase(Locale.ENGLISH).charAt(0) != d.charAt(endIndex)) { return -1; } bufferIndex--; endIndex--; } return bufferIndex + 1; } return currentLine.trim().toLowerCase(Locale.ENGLISH).equals(d) ? buf.length() - currentLine.length() : -1; } private void printWarnings(SQLWarning warning, boolean force) throws SQLException { SQLWarning initialWarning = warning; if (showWarnings || force) { while (warning != null) { log(warning + " sql warning", showWarnings ? Project.MSG_WARN : Project.MSG_VERBOSE); warning = warning.getNextWarning(); } } if (initialWarning != null) { setWarningProperty(); } if (treatWarningsAsErrors && initialWarning != null) { throw initialWarning; } } protected final void setErrorProperty() { setProperty(errorProperty, "true"); } protected final void setWarningProperty() { setProperty(warningProperty, "true"); } protected final void setRowCountProperty(int rowCount) { setProperty(rowCountProperty, Integer.toString(rowCount)); } private void setProperty(String name, String value) { if (name != null) { getProject().setNewProperty(name, value); } } }