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 }