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