View Javadoc

1   package org.apache.torque.mojo;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import static org.apache.commons.lang.StringUtils.isEmpty;
23  import static org.apache.commons.lang.StringUtils.split;
24  
25  import java.io.File;
26  import java.lang.reflect.InvocationTargetException;
27  import java.sql.Connection;
28  import java.sql.SQLException;
29  import java.util.Collection;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.Vector;
33  
34  import org.apache.commons.beanutils.BeanUtils;
35  import org.apache.commons.lang.StringUtils;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.settings.Server;
38  import org.apache.maven.shared.filtering.MavenFileFilter;
39  import org.apache.torque.engine.platform.Platform;
40  import org.apache.torque.engine.platform.PlatformFactory;
41  import org.apache.torque.util.JdbcConfigurer;
42  import org.apache.torque.util.MojoDatabaseListener;
43  import org.kuali.core.db.torque.PropertyHandlingException;
44  import org.kuali.core.db.torque.Utils;
45  import org.kuali.db.jdbc.ConnectionHandler;
46  import org.kuali.db.jdbc.Credentials;
47  import org.kuali.db.jdbc.JDBCUtils;
48  import org.kuali.db.jdbc.SQLExecutor;
49  import org.kuali.db.jdbc.Transaction;
50  
51  /**
52   * Abstract mojo for making use of SQLExecutor
53   */
54  public abstract class AbstractSQLExecutorMojo extends BaseMojo {
55      Utils utils = new Utils();
56      JDBCUtils jdbcUtils;
57      ConnectionHandler connectionHandler;
58      Platform platform;
59  
60      public static final String DRIVER_INFO_PROPERTIES_USER = "user";
61      public static final String DRIVER_INFO_PROPERTIES_PASSWORD = "password";
62  
63      /**
64       * Call {@link #setOrder(String)} with this value to sort in ascendant order the sql files.
65       */
66      public static final String FILE_SORTING_ASC = "ascending";
67  
68      /**
69       * Call {@link #setOrder(String)} with this value to sort in descendant order the sql files.
70       */
71      public static final String FILE_SORTING_DSC = "descending";
72  
73      // ////////////////////////// User Info ///////////////////////////////////
74  
75      /**
76       * The type of database we are targeting (eg oracle, mysql). This is optional if <code>url</code> is supplied as the
77       * database type will be automatically detected based on the <code>url</code>. If targetDatabase is explicitly
78       * supplied it will override the type selected by the automatic detection logic.
79       *
80       * @parameter expression="${targetDatabase}"
81       */
82      String targetDatabase;
83  
84      /**
85       * Database username. If not given, it will be looked up through <code>settings.xml</code>'s server with
86       * <code>${settingsKey}</code> as key.
87       *
88       * @parameter expression="${username}"
89       */
90      String username;
91  
92      /**
93       * Database password. If not given, it will be looked up through <code>settings.xml</code>'s server with
94       * <code>${settingsKey}</code> as key.
95       *
96       * @parameter expression="${password}"
97       */
98      String password;
99  
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 &quot;;&quot;;
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 }