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 }