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 &quot;;&quot;;
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    }