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