001    /**
002     * Copyright 2004-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.apache.torque.mojo;
017    
018    import java.io.File;
019    import java.io.IOException;
020    import java.io.OutputStream;
021    import java.util.ArrayList;
022    import java.util.List;
023    
024    import org.apache.commons.io.FileUtils;
025    import org.apache.commons.io.IOUtils;
026    import org.apache.commons.lang.StringUtils;
027    import org.apache.maven.plugin.AbstractMojo;
028    import org.apache.maven.plugin.MojoExecutionException;
029    import org.apache.torque.util.SimpleScanner;
030    
031    /**
032     * Examine SQL files and convert them to always use the delimiter <code>/</code> on it's own line. Convert any lines ending with
033     * <code>;</code> to use a <code>/</code> on it's own line.
034     *
035     * @goal convertsql
036     */
037    public class ConvertSQLMojo extends AbstractMojo {
038            private static final String FS = File.separator;
039    
040            /**
041             * The encoding to use when reading/writing files
042             *
043             * @parameter expression="${impex.encoding}" default-value="${project.build.sourceEncoding}"
044             */
045            private String encoding;
046    
047            /**
048             * The old delimiter used to terminate a SQL statement by being present at the end of a line.
049             *
050             * @parameter expression="${impex.oldDelimiter}" default-value=";"
051             * @required
052             */
053            private String oldDelimiter;
054    
055            /**
056             * The new delimiter to put on its own line
057             *
058             * @parameter expression="${impex.newDelimiter}" default-value="/"
059             * @required
060             */
061            private String newDelimiter;
062    
063            /**
064             * Directory to examine for .sql files
065             *
066             * @parameter expression="${impex.sourceDir}" default-value="${project.build.directory}/sql/source"
067             * @required
068             */
069            private File sourceDir;
070    
071            /**
072             * Directory to generate the converted files into
073             *
074             * @parameter expression="${impex.outputDir}" default-value="${project.build.directory}/sql/output"
075             * @required
076             */
077            private File outputDir;
078    
079            /**
080             * CSV list of regex patterns for files to include
081             *
082             * @parameter expression="${impex.includes}" default-value="\*\*\/*.sql"
083             * @required
084             */
085            private String includes;
086    
087            /**
088             * CSV list of regex patterns for files to exclude
089             *
090             * @parameter expression="${impex.excludes}" default-value="\*\*\/.svn/**,\*\*\/.git/**"
091             */
092            private String excludes;
093    
094            /**
095             * Liquibase injects a handful of metadata at the top of each SQL file that causes "noise". All of the checksums and actual SQL may stay
096             * the same, but the Liquibase metadata makes it appear as though the resulting SQL file has changed. By omitting the metadata you can
097             * run the same change set multiple times and get the exact same SQL file as output. This makes it much easier to perform validation
098             * checks on the SQL.
099             *
100             * @parameter expression="${impex.skipIrrelevantLiquibaseMetadataLines}" default-value="false"
101             */
102            private boolean skipIrrelevantLiquibaseMetadataLines;
103    
104            private List<String> liquibaseTokens = getLiquibaseTokens();
105    
106            @Override
107            public void execute() throws MojoExecutionException {
108                    try {
109                            // Make sure the sourceDir exists
110                            FileUtils.forceMkdir(sourceDir);
111                            getLog().info("Source Dir - " + sourceDir.getCanonicalPath());
112                            getLog().info("Output Dir - " + outputDir.getCanonicalPath());
113                            getLog().info("Includes - " + includes);
114                            getLog().info("Excludes - " + excludes);
115                            getLog().info("Old Delimiter - " + oldDelimiter);
116                            getLog().info("New Delimiter - " + newDelimiter);
117                            getLog().info("Encoding - " + encoding);
118                            List<File> files = getFiles();
119                            if (files == null || files.size() == 0) {
120                                    getLog().info("No files found");
121                                    return;
122                            }
123                            getLog().info("Found " + files.size() + " SQL files to convert");
124                            convert(files);
125                    } catch (Exception e) {
126                            throw new MojoExecutionException("Unexpected error", e);
127                    }
128            }
129    
130            protected void convert(List<File> files) throws IOException {
131                    for (File file : files) {
132                            convert(file);
133                    }
134            }
135    
136            protected List<String> getLiquibaseTokens() {
137                    List<String> tokens = new ArrayList<String>();
138                    tokens.add("-- Ran at:");
139                    tokens.add("-- Against:");
140                    tokens.add("-- Liquibase version:");
141                    tokens.add("--  Ran at:");
142                    tokens.add("--  Against:");
143                    tokens.add("--  Liquibase version:");
144                    return tokens;
145            }
146    
147            protected boolean isSkipLine(String line) {
148                    if (!skipIrrelevantLiquibaseMetadataLines) {
149                            return false;
150                    }
151                    for (String token : liquibaseTokens) {
152                            if (line.startsWith(token)) {
153                                    return true;
154                            }
155                    }
156                    return false;
157            }
158    
159            protected String getRelativePath(File dir, File file) throws IOException {
160                    String dirPath = dir.getCanonicalPath() + FS;
161                    String filePath = file.getCanonicalPath();
162                    return StringUtils.remove(filePath, dirPath);
163            }
164    
165            protected void convert(File file) throws IOException {
166                    String outputFilename = outputDir + FS + getRelativePath(sourceDir, file);
167                    File outputFile = new File(outputFilename);
168                    @SuppressWarnings("unchecked")
169                    List<String> lines = FileUtils.readLines(file, encoding);
170                    getLog().info("Writing " + outputFile.getCanonicalPath());
171                    OutputStream out = null;
172                    try {
173                            out = FileUtils.openOutputStream(outputFile);
174                            for (String line : lines) {
175                                    if (isSkipLine(line)) {
176                                            continue;
177                                    }
178                                    String convertedLine = getConvertedLine(line);
179                                    out.write(convertedLine.getBytes(encoding));
180                            }
181                    } finally {
182                            IOUtils.closeQuietly(out);
183                    }
184            }
185    
186            protected String getConvertedLine(String line) {
187                    String trimmed = StringUtils.trimToNull(line);
188                    if (StringUtils.endsWith(trimmed, oldDelimiter)) {
189                            int pos = line.lastIndexOf(oldDelimiter);
190                            return line.substring(0, pos) + IOUtils.LINE_SEPARATOR_UNIX + newDelimiter + IOUtils.LINE_SEPARATOR_UNIX;
191                    } else {
192                            return line + IOUtils.LINE_SEPARATOR_UNIX;
193                    }
194            }
195    
196            protected List<File> getFiles() throws IOException {
197                    getLog().info("Examining " + sourceDir.getCanonicalPath());
198                    String[] includeTokens = StringUtils.split(includes, ",");
199                    String[] excludeTokens = StringUtils.split(excludes, ",");
200                    SimpleScanner scanner = new SimpleScanner(sourceDir, includeTokens, excludeTokens);
201                    return scanner.getFiles();
202            }
203    
204            public File getSourceDir() {
205                    return sourceDir;
206            }
207    
208            public void setSourceDir(File inputDir) {
209                    this.sourceDir = inputDir;
210            }
211    
212            public File getOutputDir() {
213                    return outputDir;
214            }
215    
216            public void setOutputDir(File outputDir) {
217                    this.outputDir = outputDir;
218            }
219    
220            public String getIncludes() {
221                    return includes;
222            }
223    
224            public void setIncludes(String includes) {
225                    this.includes = includes;
226            }
227    
228            public String getExcludes() {
229                    return excludes;
230            }
231    
232            public void setExcludes(String excludes) {
233                    this.excludes = excludes;
234            }
235    
236            public String getEncoding() {
237                    return encoding;
238            }
239    
240            public void setEncoding(String encoding) {
241                    this.encoding = encoding;
242            }
243    
244            public String getOldDelimiter() {
245                    return oldDelimiter;
246            }
247    
248            public void setOldDelimiter(String oldDelimiter) {
249                    this.oldDelimiter = oldDelimiter;
250            }
251    
252            public String getNewDelimiter() {
253                    return newDelimiter;
254            }
255    
256            public void setNewDelimiter(String newDelimiter) {
257                    this.newDelimiter = newDelimiter;
258            }
259    
260            public boolean isSkipIrrelevantLiquibaseMetadataLines() {
261                    return skipIrrelevantLiquibaseMetadataLines;
262            }
263    
264            public void setSkipIrrelevantLiquibaseMetadataLines(boolean skipIrrelevantLiquibaseMetadataLines) {
265                    this.skipIrrelevantLiquibaseMetadataLines = skipIrrelevantLiquibaseMetadataLines;
266            }
267    
268    }