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 }