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