001 package org.apache.torque.mojo; 002 003 /* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022 import static org.apache.commons.lang.StringUtils.isEmpty; 023 import static org.apache.commons.lang.StringUtils.split; 024 025 import java.io.File; 026 import java.lang.reflect.InvocationTargetException; 027 import java.sql.Connection; 028 import java.sql.SQLException; 029 import java.util.Collection; 030 import java.util.Map; 031 import java.util.Properties; 032 import java.util.Vector; 033 034 import org.apache.commons.beanutils.BeanUtils; 035 import org.apache.commons.lang.StringUtils; 036 import org.apache.maven.plugin.MojoExecutionException; 037 import org.apache.maven.settings.Server; 038 import org.apache.maven.shared.filtering.MavenFileFilter; 039 import org.apache.torque.engine.platform.Platform; 040 import org.apache.torque.engine.platform.PlatformFactory; 041 import org.apache.torque.util.JdbcConfigurer; 042 import org.apache.torque.util.MojoDatabaseListener; 043 import org.kuali.core.db.torque.PropertyHandlingException; 044 import org.kuali.core.db.torque.Utils; 045 import org.kuali.db.jdbc.ConnectionHandler; 046 import org.kuali.db.jdbc.Credentials; 047 import org.kuali.db.jdbc.JDBCUtils; 048 import org.kuali.db.jdbc.SQLExecutor; 049 import org.kuali.db.jdbc.Transaction; 050 051 /** 052 * Abstract mojo for making use of SQLExecutor 053 */ 054 public abstract class AbstractSQLExecutorMojo extends BaseMojo { 055 Utils utils = new Utils(); 056 JDBCUtils jdbcUtils; 057 ConnectionHandler connectionHandler; 058 Platform platform; 059 060 public static final String DRIVER_INFO_PROPERTIES_USER = "user"; 061 public static final String DRIVER_INFO_PROPERTIES_PASSWORD = "password"; 062 063 /** 064 * Call {@link #setOrder(String)} with this value to sort in ascendant order the sql files. 065 */ 066 public static final String FILE_SORTING_ASC = "ascending"; 067 068 /** 069 * Call {@link #setOrder(String)} with this value to sort in descendant order the sql files. 070 */ 071 public static final String FILE_SORTING_DSC = "descending"; 072 073 // ////////////////////////// User Info /////////////////////////////////// 074 075 /** 076 * The type of database we are targeting (eg oracle, mysql). This is optional if <code>url</code> is supplied as the 077 * database type will be automatically detected based on the <code>url</code>. If targetDatabase is explicitly 078 * supplied it will override the type selected by the automatic detection logic. 079 * 080 * @parameter expression="${targetDatabase}" 081 */ 082 String targetDatabase; 083 084 /** 085 * Database username. If not given, it will be looked up through <code>settings.xml</code>'s server with 086 * <code>${settingsKey}</code> as key. 087 * 088 * @parameter expression="${username}" 089 */ 090 String username; 091 092 /** 093 * Database password. If not given, it will be looked up through <code>settings.xml</code>'s server with 094 * <code>${settingsKey}</code> as key. 095 * 096 * @parameter expression="${password}" 097 */ 098 String password; 099 100 /** 101 * Ignore the password and use anonymous access. 102 * 103 * @parameter expression="${enableAnonymousPassword}" default-value="false" 104 */ 105 boolean enableAnonymousPassword; 106 107 /** 108 * Ignore the username and use anonymous access. 109 * 110 * @parameter expression="${enableAnonymousUsername}" default-value="false" 111 */ 112 boolean enableAnonymousUsername; 113 114 /** 115 * Additional key=value pairs separated by a comma to be passed to JDBC driver. 116 * 117 * @parameter expression="${driverProperties}" default-value="" 118 */ 119 String driverProperties; 120 121 /** 122 * If set to true the password being used to connect to the database will be displayed in log messages. 123 * 124 * @parameter expression="${showPassword}" default-value="false" 125 */ 126 boolean showPassword; 127 128 /** 129 * The id of the server in settings.xml containing the username/password to use. 130 * 131 * @parameter expression="${settingsKey}" default-value="impex.${project.artifactId}" 132 */ 133 String settingsKey; 134 135 /** 136 * Skip execution if there is an error obtaining a connection. If this is set to true, the build will continue even 137 * if there is an error obtaining a connection 138 * 139 * @parameter expression="${skipOnConnectionError}" default-value="false" 140 */ 141 boolean skipOnConnectionError; 142 143 /** 144 * SQL input commands separated by <code>${delimiter}</code>. 145 * 146 * @parameter expression="${sqlCommand}" default-value="" 147 */ 148 String sqlCommand = ""; 149 150 /** 151 * List of files containing SQL statements to load. 152 * 153 * @parameter expression="${srcFiles}" 154 */ 155 File[] srcFiles; 156 157 // //////////////////////////////// Database info ///////////////////////// 158 /** 159 * Database URL. 160 * 161 * @parameter expression="${url}" 162 */ 163 String url; 164 165 /** 166 * Database driver classname. This parameter is optional, as the correct JDBC driver to use is detected from the 167 * <code>url</code> in almost all cases (works for Oracle, MySQL, Derby, PostGresSQL, DB2, H2, HSQL, SQL Server). If 168 * a driver is explicitly supplied, it will be used in place of the JDBC driver the automatic detection logic would 169 * have chosen. 170 * 171 * @parameter expression="${driver}" 172 */ 173 String driver; 174 175 // //////////////////////////// Operation Configuration //////////////////// 176 /** 177 * Set to <code>true</code> to execute non-transactional SQL. 178 * 179 * @parameter expression="${autocommit}" default-value="false" 180 */ 181 boolean autocommit; 182 183 /** 184 * Action to perform if an error is found. Possible values are <code>abort</code> and <code>continue</code>. 185 * 186 * @parameter expression="${onError}" default-value="abort" 187 */ 188 String onError = SQLExecutor.ON_ERROR_ABORT; 189 190 // //////////////////////////// Parser Configuration //////////////////// 191 192 /** 193 * Set the delimiter that separates SQL statements. 194 * 195 * @parameter expression="${delimiter}" default-value="/" 196 */ 197 String delimiter = "/"; 198 199 /** 200 * The delimiter type takes two values - "normal" and "row". Normal means that any occurrence of the delimiter 201 * terminates the SQL command whereas with row, only a line containing just the delimiter is recognized as the end 202 * of the command.<br> 203 * <br> 204 * For example, set delimiterType to "row" and delimiter to "/" for Oracle 205 * 206 * @parameter expression="${delimiterType}" default-value="row" 207 */ 208 String delimiterType = DelimiterType.ROW; 209 210 /** 211 * Keep the format of an SQL block. 212 * 213 * @parameter expression="${keepFormat}" default-value="true" 214 */ 215 boolean keepFormat = true; 216 217 /** 218 * Print header columns. 219 * 220 * @parameter expression="${showheaders}" default-value="true" 221 */ 222 boolean showheaders = true; 223 224 /** 225 * If writing output to a file, append to an existing file or overwrite it? 226 * 227 * @parameter expression="${append}" default-value="false" 228 */ 229 boolean append = false; 230 231 /** 232 * Argument to Statement.setEscapeProcessing If you want the driver to use regular SQL syntax then set this to 233 * false. 234 * 235 * @parameter expression="${escapeProcessing}" default-value="true" 236 */ 237 boolean escapeProcessing = true; 238 239 // //////////////////////////////// Internal properties////////////////////// 240 241 /** 242 * number of successful executed statements 243 */ 244 int successfulStatements = 0; 245 246 /** 247 * number of total executed statements 248 */ 249 int totalStatements = 0; 250 251 /** 252 * Database connection 253 */ 254 Connection conn = null; 255 256 /** 257 * SQL transactions to perform 258 */ 259 Vector<Transaction> transactions = new Vector<Transaction>(); 260 261 /** 262 * @component role="org.apache.maven.shared.filtering.MavenFileFilter" 263 */ 264 MavenFileFilter fileFilter; 265 266 /** 267 * The credentials to use for database access 268 */ 269 Credentials credentials; 270 271 protected void configureTransactions() throws MojoExecutionException { 272 // default implementation does nothing 273 } 274 275 protected Properties getContextProperties() { 276 Properties properties = new Properties(); 277 Map<String, String> environment = System.getenv(); 278 for (String key : environment.keySet()) { 279 properties.put("env." + key, environment.get(key)); 280 } 281 properties.putAll(getProject().getProperties()); 282 properties.putAll(System.getProperties()); 283 return properties; 284 } 285 286 protected Credentials getNewCredentials() { 287 Credentials credentials = new Credentials(); 288 credentials.setUsername(getUsername()); 289 credentials.setPassword(getPassword()); 290 return credentials; 291 } 292 293 protected ConnectionHandler getNewConnectionHandler() throws MojoExecutionException { 294 ConnectionHandler connectionHandler = new ConnectionHandler(); 295 try { 296 BeanUtils.copyProperties(connectionHandler, this); 297 return connectionHandler; 298 } catch (Exception e) { 299 throw new MojoExecutionException("Error establishing connection", e); 300 } 301 } 302 303 /** 304 * Validate our configuration and execute SQL as appropriate 305 * 306 * @throws MojoExecutionException 307 */ 308 @Override 309 public void executeMojo() throws MojoExecutionException { 310 jdbcUtils = new JDBCUtils(); 311 updateConfiguration(); 312 Credentials credentials = getNewCredentials(); 313 updateCredentials(credentials); 314 validateCredentials(credentials); 315 setCredentials(credentials); 316 validateConfiguration(); 317 318 connectionHandler = getNewConnectionHandler(); 319 conn = getConnection(); 320 321 if (connectionHandler.isConnectionError() && skipOnConnectionError) { 322 // There was an error obtaining a connection 323 // Do not fail the build but don't do anything more 324 return; 325 } 326 327 // Configure the transactions we will be running 328 configureTransactions(); 329 330 // Make sure our counters are zeroed out 331 successfulStatements = 0; 332 totalStatements = 0; 333 334 // Get an SQLExecutor 335 SQLExecutor executor = getSqlExecutor(); 336 337 try { 338 executor.execute(); 339 } catch (SQLException e) { 340 throw new MojoExecutionException("Error executing SQL", e); 341 } 342 } 343 344 /** 345 * Set an inline SQL command to execute. 346 * 347 * @param sql 348 * the sql statement to add 349 */ 350 public void addText(final String sql) { 351 this.sqlCommand += sql; 352 } 353 354 /** 355 * Set the delimiter that separates SQL statements. Defaults to ";"; 356 * 357 * @param delimiter 358 * the new delimiter 359 */ 360 public void setDelimiter(final String delimiter) { 361 this.delimiter = delimiter; 362 } 363 364 /** 365 * Set the delimiter type: "normal" or "row" (default "normal"). 366 * 367 * @param delimiterType 368 * the new delimiterType 369 */ 370 public void setDelimiterType(final String delimiterType) { 371 this.delimiterType = delimiterType; 372 } 373 374 /** 375 * Print headers for result sets from the statements; optional, default true. 376 * 377 * @param showheaders 378 * <code>true</code> to show the headers, otherwise <code>false</code> 379 */ 380 public void setShowheaders(final boolean showheaders) { 381 this.showheaders = showheaders; 382 } 383 384 /** 385 * whether output should be appended to or overwrite an existing file. Defaults to false. 386 * 387 * @param append 388 * <code>true</code> to append, otherwise <code>false</code> to overwrite 389 */ 390 public void setAppend(final boolean append) { 391 this.append = append; 392 } 393 394 /** 395 * whether or not format should be preserved. Defaults to false. 396 * 397 * @param keepformat 398 * The keepformat to set 399 */ 400 public void setKeepFormat(final boolean keepformat) { 401 this.keepFormat = keepformat; 402 } 403 404 /** 405 * Set escape processing for statements. 406 * 407 * @param enable 408 * <code>true</code> to escape, otherwiser <code>false</code> 409 */ 410 public void setEscapeProcessing(final boolean enable) { 411 escapeProcessing = enable; 412 } 413 414 protected SQLExecutor getSqlExecutor() throws MojoExecutionException { 415 try { 416 SQLExecutor executor = new SQLExecutor(); 417 BeanUtils.copyProperties(executor, this); 418 executor.addListener(new MojoDatabaseListener(getLog())); 419 return executor; 420 } catch (InvocationTargetException e) { 421 throw new MojoExecutionException("Error copying properties from the mojo to the SQL executor", e); 422 } catch (IllegalAccessException e) { 423 throw new MojoExecutionException("Error copying properties from the mojo to the SQL executor", e); 424 } 425 } 426 427 /** 428 * Attempt to automatically detect the correct JDBC driver and database type (oracle, mysql, h2, derby, etc) given a 429 * JDBC url 430 */ 431 protected void updateConfiguration() throws MojoExecutionException { 432 try { 433 new JdbcConfigurer().updateConfiguration(this); 434 } catch (PropertyHandlingException e) { 435 throw new MojoExecutionException("Error handling properties", e); 436 } 437 platform = PlatformFactory.getPlatformFor(targetDatabase); 438 } 439 440 /** 441 * Validate that some essential configuration items are present 442 */ 443 protected void validateConfiguration() throws MojoExecutionException { 444 new JdbcConfigurer().validateConfiguration(this); 445 } 446 447 protected void validateCredentials(final Credentials credentials, final boolean anonymousAccessAllowed, 448 final String validationFailureMessage) throws MojoExecutionException { 449 if (anonymousAccessAllowed) { 450 // If credentials aren't required, don't bother validating 451 return; 452 } 453 String username = credentials.getUsername(); 454 String password = credentials.getPassword(); 455 if (!isEmpty(username) && !isEmpty(password)) { 456 // Both are required, and both have been supplied 457 return; 458 } 459 throw new MojoExecutionException(validationFailureMessage); 460 } 461 462 protected void validateCredentials(final Credentials credentials) throws MojoExecutionException { 463 // Both are required but one (or both) are missing 464 StringBuffer sb = new StringBuffer(); 465 sb.append("\n\n"); 466 sb.append("Username and password must be specified.\n"); 467 sb.append("Specify them in the plugin configuration or as a system property.\n"); 468 sb.append("\n"); 469 sb.append("For example:\n"); 470 sb.append("-Dusername=myuser\n"); 471 sb.append("-Dpassword=mypassword\n"); 472 sb.append("\n."); 473 validateCredentials(credentials, enableAnonymousUsername && enableAnonymousPassword, sb.toString()); 474 } 475 476 protected boolean isNullOrEmpty(final Collection<?> c) { 477 if (c == null) { 478 return true; 479 } 480 if (c.size() == 0) { 481 return true; 482 } 483 return false; 484 } 485 486 protected String convertNullToEmpty(final String s) { 487 if (s == null) { 488 return ""; 489 } else { 490 return s; 491 } 492 } 493 494 /** 495 * Load username/password from settings.xml if user has not set them in JVM properties 496 * 497 * @throws MojoExecutionException 498 */ 499 protected void updateCredentials(final Credentials credentials) { 500 Server server = getServerFromSettingsKey(); 501 String username = getUpdatedUsername(server, credentials.getUsername()); 502 String password = getUpdatedPassword(server, credentials.getPassword()); 503 credentials.setUsername(convertNullToEmpty(username)); 504 credentials.setPassword(convertNullToEmpty(password)); 505 } 506 507 protected Server getServerFromSettingsKey() { 508 Server server = getSettings().getServer(getSettingsKey()); 509 if (server == null) { 510 // Fall through to using the JDBC url as a key 511 return getSettings().getServer("impex." + getUrl()); 512 } else { 513 return null; 514 } 515 } 516 517 protected String getUpdatedPassword(final Server server, final String password) { 518 // They already gave us a password, don't mess with it 519 if (!isEmpty(password)) { 520 return password; 521 } 522 if (server != null) { 523 // We've successfully located a server in settings.xml, use the password from that 524 getLog().info( 525 "Located a password in settings.xml under the server id '" + server.getId() + "' Password: " 526 + getDisplayPassword(server.getPassword())); 527 return server.getPassword(); 528 } 529 getLog().info("Using default password generated from the artifact id"); 530 return platform.getSchemaName(getProject().getArtifactId()); 531 } 532 533 protected String getDisplayPassword(final String password) { 534 if (isShowPassword()) { 535 return password; 536 } else { 537 return StringUtils.repeat("*", password.length()); 538 } 539 } 540 541 protected String getUpdatedUsername(final Server server, final String username) { 542 // They already gave us a username, don't mess with it 543 if (!isEmpty(username)) { 544 return username; 545 } 546 if (server != null) { 547 // We've successfully located a server in settings.xml, use the username from that 548 getLog().info( 549 "Located a username in settings.xml under the server id '" + server.getId() + "' Username: " 550 + server.getUsername()); 551 return server.getUsername(); 552 } 553 getLog().info("Using default username generated from the artifact id"); 554 return platform.getSchemaName(getProject().getArtifactId()); 555 } 556 557 /** 558 * Creates a new Connection as using the driver, url, userid and password specified. The calling method is 559 * responsible for closing the connection. 560 * 561 * @return Connection the newly created connection. 562 * @throws MojoExecutionException 563 * if the UserId/Password/Url is not set or there is no suitable driver or the driver fails to load. 564 * @throws SQLException 565 * if there is problem getting connection with valid url 566 */ 567 protected Connection getConnection() throws MojoExecutionException { 568 try { 569 return connectionHandler.getConnection(); 570 } catch (Exception e) { 571 throw new MojoExecutionException("Error establishing connection", e); 572 } 573 } 574 575 /** 576 * parse driverProperties into Properties set 577 * 578 * @return the driver properties 579 * @throws MojoExecutionException 580 */ 581 protected Properties getDriverProperties() throws MojoExecutionException { 582 Properties properties = new Properties(); 583 584 if (isEmpty(this.driverProperties)) { 585 return properties; 586 } 587 588 String[] tokens = split(this.driverProperties, ","); 589 for (int i = 0; i < tokens.length; ++i) { 590 String[] keyValueTokens = split(tokens[i].trim(), "="); 591 if (keyValueTokens.length != 2) { 592 throw new MojoExecutionException("Invalid JDBC Driver properties: " + this.driverProperties); 593 } 594 properties.setProperty(keyValueTokens[0], keyValueTokens[1]); 595 } 596 return properties; 597 } 598 599 public String getUsername() { 600 return this.username; 601 } 602 603 public void setUsername(final String username) { 604 this.username = username; 605 } 606 607 public String getPassword() { 608 return this.password; 609 } 610 611 public void setPassword(final String password) { 612 this.password = password; 613 } 614 615 public String getUrl() { 616 return this.url; 617 } 618 619 public void setUrl(final String url) { 620 this.url = url; 621 } 622 623 public String getDriver() { 624 return this.driver; 625 } 626 627 public void setDriver(final String driver) { 628 this.driver = driver; 629 } 630 631 public void setAutocommit(final boolean autocommit) { 632 this.autocommit = autocommit; 633 } 634 635 public File[] getSrcFiles() { 636 return this.srcFiles; 637 } 638 639 public void setSrcFiles(final File[] files) { 640 this.srcFiles = files; 641 } 642 643 /** 644 * Number of SQL statements executed so far that caused errors. 645 * 646 * @return the number 647 */ 648 public int getSuccessfulStatements() { 649 return successfulStatements; 650 } 651 652 /** 653 * Number of SQL statements executed so far, including the ones that caused errors. 654 * 655 * @return the number 656 */ 657 public int getTotalStatements() { 658 return totalStatements; 659 } 660 661 public String getOnError() { 662 return this.onError; 663 } 664 665 public void setOnError(final String action) { 666 if (SQLExecutor.ON_ERROR_ABORT.equalsIgnoreCase(action)) { 667 this.onError = SQLExecutor.ON_ERROR_ABORT; 668 } else if (SQLExecutor.ON_ERROR_CONTINUE.equalsIgnoreCase(action)) { 669 this.onError = SQLExecutor.ON_ERROR_CONTINUE; 670 } else if (SQLExecutor.ON_ERROR_ABORT_AFTER.equalsIgnoreCase(action)) { 671 this.onError = SQLExecutor.ON_ERROR_ABORT_AFTER; 672 } else { 673 throw new IllegalArgumentException(action + " is not a valid value for onError, only '" 674 + SQLExecutor.ON_ERROR_ABORT + "', '" + SQLExecutor.ON_ERROR_ABORT_AFTER + "', or '" 675 + SQLExecutor.ON_ERROR_CONTINUE + "'."); 676 } 677 } 678 679 public void setSettingsKey(final String key) { 680 this.settingsKey = key; 681 } 682 683 public void setDriverProperties(final String driverProperties) { 684 this.driverProperties = driverProperties; 685 } 686 687 public String getSqlCommand() { 688 return sqlCommand; 689 } 690 691 public void setSqlCommand(final String sqlCommand) { 692 this.sqlCommand = sqlCommand; 693 } 694 695 public Vector<Transaction> getTransactions() { 696 return transactions; 697 } 698 699 public void setTransactions(final Vector<Transaction> transactions) { 700 this.transactions = transactions; 701 } 702 703 public void setFileFilter(final MavenFileFilter filter) { 704 this.fileFilter = filter; 705 } 706 707 public String getTargetDatabase() { 708 return targetDatabase; 709 } 710 711 public void setTargetDatabase(final String targetDatabase) { 712 this.targetDatabase = targetDatabase; 713 } 714 715 public Connection getConn() { 716 return conn; 717 } 718 719 public void setConn(final Connection conn) { 720 this.conn = conn; 721 } 722 723 public String getDelimiter() { 724 return delimiter; 725 } 726 727 public String getDelimiterType() { 728 return delimiterType; 729 } 730 731 public boolean isKeepFormat() { 732 return keepFormat; 733 } 734 735 public boolean isShowheaders() { 736 return showheaders; 737 } 738 739 public boolean isAppend() { 740 return append; 741 } 742 743 public boolean isEscapeProcessing() { 744 return escapeProcessing; 745 } 746 747 public boolean isSkipOnConnectionError() { 748 return skipOnConnectionError; 749 } 750 751 public void setSkipOnConnectionError(final boolean skipOnConnectionError) { 752 this.skipOnConnectionError = skipOnConnectionError; 753 } 754 755 public MavenFileFilter getFileFilter() { 756 return fileFilter; 757 } 758 759 public boolean isShowPassword() { 760 return showPassword; 761 } 762 763 public void setShowPassword(final boolean showPassword) { 764 this.showPassword = showPassword; 765 } 766 767 public boolean isEnableAnonymousPassword() { 768 return enableAnonymousPassword; 769 } 770 771 public void setEnableAnonymousPassword(final boolean enableAnonymousPassword) { 772 this.enableAnonymousPassword = enableAnonymousPassword; 773 } 774 775 public String getSettingsKey() { 776 return settingsKey; 777 } 778 779 public boolean isAutocommit() { 780 return autocommit; 781 } 782 783 public void setSuccessfulStatements(final int successfulStatements) { 784 this.successfulStatements = successfulStatements; 785 } 786 787 public void setTotalStatements(final int totalStatements) { 788 this.totalStatements = totalStatements; 789 } 790 791 public void setCredentials(final Credentials credentials) { 792 this.credentials = credentials; 793 } 794 795 public Credentials getCredentials() { 796 return credentials; 797 } 798 799 }