View Javadoc
1   /*
2    *    Copyright 2010-2023 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.apache.ibatis.migration.commands;
17  
18  import static org.apache.ibatis.migration.utils.Util.file;
19  
20  import java.io.File;
21  import java.io.FileReader;
22  import java.io.FileWriter;
23  import java.io.IOException;
24  import java.io.LineNumberReader;
25  import java.io.OutputStream;
26  import java.io.PrintStream;
27  import java.io.PrintWriter;
28  import java.io.Reader;
29  import java.net.URL;
30  import java.net.URLClassLoader;
31  import java.text.DecimalFormat;
32  import java.text.ParseException;
33  import java.text.SimpleDateFormat;
34  import java.util.ArrayList;
35  import java.util.Date;
36  import java.util.List;
37  import java.util.Properties;
38  import java.util.ServiceLoader;
39  import java.util.TimeZone;
40  
41  import org.apache.ibatis.migration.Change;
42  import org.apache.ibatis.migration.ConnectionProvider;
43  import org.apache.ibatis.migration.Environment;
44  import org.apache.ibatis.migration.FileMigrationLoader;
45  import org.apache.ibatis.migration.FileMigrationLoaderFactory;
46  import org.apache.ibatis.migration.JdbcConnectionProvider;
47  import org.apache.ibatis.migration.MigrationException;
48  import org.apache.ibatis.migration.MigrationLoader;
49  import org.apache.ibatis.migration.VariableReplacer;
50  import org.apache.ibatis.migration.hook.FileHookScriptFactory;
51  import org.apache.ibatis.migration.hook.FileMigrationHook;
52  import org.apache.ibatis.migration.hook.HookScriptFactory;
53  import org.apache.ibatis.migration.hook.MigrationHook;
54  import org.apache.ibatis.migration.io.Resources;
55  import org.apache.ibatis.migration.options.DatabaseOperationOption;
56  import org.apache.ibatis.migration.options.Options;
57  import org.apache.ibatis.migration.options.SelectedOptions;
58  import org.apache.ibatis.migration.options.SelectedPaths;
59  import org.apache.ibatis.migration.utils.Util;
60  
61  public abstract class BaseCommand implements Command {
62    private static final String DATE_FORMAT = "yyyyMMddHHmmss";
63    protected static final String DESC_CREATE_CHANGELOG = "create changelog";
64  
65    private ClassLoader driverClassLoader;
66    private Environment environment;
67  
68    protected PrintStream printStream = System.out;
69  
70    protected final SelectedOptions options;
71    protected final SelectedPaths paths;
72  
73    protected BaseCommand(SelectedOptions selectedOptions) {
74      this.options = selectedOptions;
75      this.paths = selectedOptions.getPaths();
76      if (options.isQuiet()) {
77        this.printStream = new PrintStream(new OutputStream() {
78          @Override
79          public void write(int b) {
80            // throw away output
81          }
82        });
83      }
84    }
85  
86    public void setDriverClassLoader(ClassLoader aDriverClassLoader) {
87      driverClassLoader = aDriverClassLoader;
88    }
89  
90    public void setPrintStream(PrintStream aPrintStream) {
91      if (options.isQuiet()) {
92        aPrintStream.println("You selected to suppress output but a PrintStream is being set");
93      }
94      printStream = aPrintStream;
95    }
96  
97    protected boolean paramsEmpty(String... params) {
98      return params == null || params.length < 1 || params[0] == null || params[0].length() < 1;
99    }
100 
101   protected String changelogTable() {
102     return environment().getVariables().getProperty(Environment.CHANGELOG, "CHANGELOG");
103   }
104 
105   protected String getNextIDAsString() {
106     try {
107       // Ensure that two subsequent calls are less likely to return the same value.
108       Thread.sleep(1000);
109     } catch (InterruptedException e) {
110       // Ignore and Restore interrupted state...
111       Thread.currentThread().interrupt();
112     }
113     String idPattern = options.getIdPattern();
114     if (idPattern == null) {
115       idPattern = Util.getPropertyOption(Options.IDPATTERN.toString().toLowerCase());
116     }
117     if (idPattern != null && !idPattern.isEmpty()) {
118       return generatePatternedId(idPattern);
119     }
120     return generateTimestampId();
121   }
122 
123   private String generatePatternedId(String pattern) {
124     DecimalFormat fmt = new DecimalFormat(pattern);
125     List<Change> migrations = getMigrationLoader().getMigrations();
126     if (migrations.isEmpty()) {
127       return fmt.format(1);
128     }
129     Change lastChange = migrations.get(migrations.size() - 1);
130     try {
131       long lastId = (Long) fmt.parse(lastChange.getId().toString());
132       lastId++;
133       return fmt.format(lastId);
134     } catch (ParseException e) {
135       throw new MigrationException(
136           "Failed to parse last id '" + lastChange.getId() + "' using the specified idPattern '" + pattern + "'");
137     }
138   }
139 
140   private String generateTimestampId() {
141     final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
142     final Date now = new Date();
143     dateFormat.setTimeZone(TimeZone.getTimeZone(environment().getTimeZone()));
144     return dateFormat.format(now);
145   }
146 
147   protected void copyResourceTo(String resource, File toFile) {
148     copyResourceTo(resource, toFile, null);
149   }
150 
151   protected void copyResourceTo(String resource, File toFile, Properties variables) {
152     printStream.println("Creating: " + toFile.getName());
153     try (Reader reader = Resources.getResourceAsReader(this.getClass().getClassLoader(), resource)) {
154       copyTemplate(reader, toFile, variables);
155     } catch (IOException e) {
156       throw new MigrationException("Error copying " + resource + " to " + toFile.getAbsolutePath() + ".  Cause: " + e,
157           e);
158     }
159   }
160 
161   protected void copyExternalResourceTo(String resource, File toFile, Properties variables) {
162     printStream.println("Creating: " + toFile.getName());
163     try {
164       File sourceFile = new File(resource);
165       copyTemplate(sourceFile, toFile, variables);
166     } catch (Exception e) {
167       throw new MigrationException("Error copying " + resource + " to " + toFile.getAbsolutePath() + ".  Cause: " + e,
168           e);
169     }
170   }
171 
172   protected static void copyTemplate(File templateFile, File toFile, Properties variables) throws IOException {
173     try (FileReader reader = new FileReader(templateFile)) {
174       copyTemplate(reader, toFile, variables);
175     }
176   }
177 
178   protected static void copyTemplate(Reader templateReader, File toFile, Properties variables) throws IOException {
179     VariableReplacer replacer = new VariableReplacer(variables);
180     try (LineNumberReader reader = new LineNumberReader(templateReader);
181         PrintWriter writer = new PrintWriter(new FileWriter(toFile))) {
182       String line;
183       while ((line = reader.readLine()) != null) {
184         line = replacer.replace(line);
185         writer.println(line);
186       }
187     }
188   }
189 
190   protected File environmentFile() {
191     return file(paths.getEnvPath(), options.getEnvironment() + ".properties");
192   }
193 
194   protected File existingEnvironmentFile() {
195     File envFile = environmentFile();
196     if (!envFile.exists()) {
197       throw new MigrationException("Environment file missing: " + envFile.getAbsolutePath());
198     }
199     return envFile;
200   }
201 
202   protected Environment environment() {
203     if (environment != null) {
204       return environment;
205     }
206     environment = new Environment(existingEnvironmentFile());
207     return environment;
208   }
209 
210   protected int getStepCountParameter(int defaultSteps, String... params) {
211     final String stringParam = params.length > 0 ? params[0] : null;
212     if (stringParam == null || "".equals(stringParam)) {
213       return defaultSteps;
214     }
215     try {
216       return Integer.parseInt(stringParam);
217     } catch (NumberFormatException e) {
218       throw new MigrationException("Invalid parameter passed to command: " + params[0]);
219     }
220   }
221 
222   protected ConnectionProvider getConnectionProvider() {
223     try {
224       return new JdbcConnectionProvider(getDriverClassLoader(), environment().getDriver(), environment().getUrl(),
225           environment().getUsername(), environment().getPassword());
226     } catch (Exception e) {
227       throw new MigrationException("Error creating ScriptRunner.  Cause: " + e, e);
228     }
229   }
230 
231   private ClassLoader getDriverClassLoader() {
232     File localDriverPath = getCustomDriverPath();
233     if (driverClassLoader != null) {
234       return driverClassLoader;
235     }
236     if (localDriverPath.exists()) {
237       try {
238         List<URL> urlList = new ArrayList<>();
239         File[] files = localDriverPath.listFiles();
240         if (files != null) {
241           for (File file : files) {
242             String filename = file.getCanonicalPath();
243             if (!filename.startsWith("/")) {
244               filename = '/' + filename;
245             }
246             urlList.add(new URL("jar:file:" + filename + "!/"));
247             urlList.add(new URL("file:" + filename));
248           }
249         }
250         URL[] urls = urlList.toArray(new URL[0]);
251         return new URLClassLoader(urls);
252       } catch (Exception e) {
253         throw new MigrationException("Error creating a driver ClassLoader. Cause: " + e, e);
254       }
255     }
256     return null;
257   }
258 
259   private File getCustomDriverPath() {
260     String customDriverPath = environment().getDriverPath();
261     if (customDriverPath != null && customDriverPath.length() > 0) {
262       return new File(customDriverPath);
263     }
264     return options.getPaths().getDriverPath();
265   }
266 
267   protected MigrationLoader getMigrationLoader() {
268     Environment env = environment();
269     MigrationLoader migrationLoader = null;
270     for (FileMigrationLoaderFactory factory : ServiceLoader.load(FileMigrationLoaderFactory.class)) {
271       if (migrationLoader != null) {
272         throw new MigrationException("Found multiple implementations of FileMigrationLoaderFactory via SPI.");
273       }
274       migrationLoader = factory.create(paths, env);
275     }
276     return migrationLoader != null ? migrationLoader
277         : new FileMigrationLoader(paths.getScriptPath(), env.getScriptCharset(), env.getVariables());
278   }
279 
280   protected MigrationHook createUpHook() {
281     String before = environment().getHookBeforeUp();
282     String beforeEach = environment().getHookBeforeEachUp();
283     String afterEach = environment().getHookAfterEachUp();
284     String after = environment().getHookAfterUp();
285     if (before == null && beforeEach == null && afterEach == null && after == null) {
286       return null;
287     }
288     return createFileMigrationHook(before, beforeEach, afterEach, after);
289   }
290 
291   protected MigrationHook createDownHook() {
292     String before = environment().getHookBeforeDown();
293     String beforeEach = environment().getHookBeforeEachDown();
294     String afterEach = environment().getHookAfterEachDown();
295     String after = environment().getHookAfterDown();
296     if (before == null && beforeEach == null && afterEach == null && after == null) {
297       return null;
298     }
299     return createFileMigrationHook(before, beforeEach, afterEach, after);
300   }
301 
302   protected MigrationHook createFileMigrationHook(String before, String beforeEach, String afterEach, String after) {
303     HookScriptFactory factory = new FileHookScriptFactory(options.getPaths(), environment(), printStream);
304     return new FileMigrationHook(factory.create(before), factory.create(beforeEach), factory.create(afterEach),
305         factory.create(after));
306   }
307 
308   protected DatabaseOperationOption getDatabaseOperationOption() {
309     DatabaseOperationOption option = new DatabaseOperationOption();
310     option.setChangelogTable(changelogTable());
311     option.setStopOnError(!options.isForce());
312     option.setThrowWarning(!options.isForce() && !environment().isIgnoreWarnings());
313     option.setEscapeProcessing(false);
314     option.setAutoCommit(environment().isAutoCommit());
315     option.setFullLineDelimiter(environment().isFullLineDelimiter());
316     option.setSendFullScript(environment().isSendFullScript());
317     option.setRemoveCRs(environment().isRemoveCrs());
318     option.setDelimiter(environment().getDelimiter());
319     return option;
320   }
321 }