1 /**
2 * Copyright 2006-2016 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 * http://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.mybatis.generator.api;
17
18 import static org.mybatis.generator.internal.util.ClassloaderUtility.getCustomClassloader;
19 import static org.mybatis.generator.internal.util.messages.Messages.getString;
20
21 import java.io.BufferedWriter;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.sql.SQLException;
27 import java.util.ArrayList;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Set;
31
32 import org.mybatis.generator.config.Configuration;
33 import org.mybatis.generator.config.Context;
34 import org.mybatis.generator.config.MergeConstants;
35 import org.mybatis.generator.exception.InvalidConfigurationException;
36 import org.mybatis.generator.exception.ShellException;
37 import org.mybatis.generator.internal.DefaultShellCallback;
38 import org.mybatis.generator.internal.ObjectFactory;
39 import org.mybatis.generator.internal.NullProgressCallback;
40 import org.mybatis.generator.internal.XmlFileMergerJaxp;
41
42 /**
43 * This class is the main interface to MyBatis generator. A typical execution of the tool involves these steps:
44 *
45 * <ol>
46 * <li>Create a Configuration object. The Configuration can be the result of a parsing the XML configuration file, or it
47 * can be created solely in Java.</li>
48 * <li>Create a MyBatisGenerator object</li>
49 * <li>Call one of the generate() methods</li>
50 * </ol>
51 *
52 * @author Jeff Butler
53 * @see org.mybatis.generator.config.xml.ConfigurationParser
54 */
55 public class MyBatisGenerator {
56
57 /** The configuration. */
58 private Configuration configuration;
59
60 /** The shell callback. */
61 private ShellCallback shellCallback;
62
63 /** The generated java files. */
64 private List<GeneratedJavaFile> generatedJavaFiles;
65
66 /** The generated xml files. */
67 private List<GeneratedXmlFile> generatedXmlFiles;
68
69 /** The warnings. */
70 private List<String> warnings;
71
72 /** The projects. */
73 private Set<String> projects;
74
75 /**
76 * Constructs a MyBatisGenerator object.
77 *
78 * @param configuration
79 * The configuration for this invocation
80 * @param shellCallback
81 * an instance of a ShellCallback interface. You may specify
82 * <code>null</code> in which case the DefaultShellCallback will
83 * be used.
84 * @param warnings
85 * Any warnings generated during execution will be added to this
86 * list. Warnings do not affect the running of the tool, but they
87 * may affect the results. A typical warning is an unsupported
88 * data type. In that case, the column will be ignored and
89 * generation will continue. You may specify <code>null</code> if
90 * you do not want warnings returned.
91 * @throws InvalidConfigurationException
92 * if the specified configuration is invalid
93 */
94 public MyBatisGenerator(Configuration configuration, ShellCallback shellCallback,
95 List<String> warnings) throws InvalidConfigurationException {
96 super();
97 if (configuration == null) {
98 throw new IllegalArgumentException(getString("RuntimeError.2")); //$NON-NLS-1$
99 } else {
100 this.configuration = configuration;
101 }
102
103 if (shellCallback == null) {
104 this.shellCallback = new DefaultShellCallback(false);
105 } else {
106 this.shellCallback = shellCallback;
107 }
108
109 if (warnings == null) {
110 this.warnings = new ArrayList<String>();
111 } else {
112 this.warnings = warnings;
113 }
114 generatedJavaFiles = new ArrayList<GeneratedJavaFile>();
115 generatedXmlFiles = new ArrayList<GeneratedXmlFile>();
116 projects = new HashSet<String>();
117
118 this.configuration.validate();
119 }
120
121 /**
122 * This is the main method for generating code. This method is long running, but progress can be provided and the
123 * method can be canceled through the ProgressCallback interface. This version of the method runs all configured
124 * contexts.
125 *
126 * @param callback
127 * an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
128 * information
129 * @throws SQLException
130 * the SQL exception
131 * @throws IOException
132 * Signals that an I/O exception has occurred.
133 * @throws InterruptedException
134 * if the method is canceled through the ProgressCallback
135 */
136 public void generate(ProgressCallback callback) throws SQLException,
137 IOException, InterruptedException {
138 generate(callback, null, null, true);
139 }
140
141 /**
142 * This is the main method for generating code. This method is long running, but progress can be provided and the
143 * method can be canceled through the ProgressCallback interface.
144 *
145 * @param callback
146 * an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
147 * information
148 * @param contextIds
149 * a set of Strings containing context ids to run. Only the contexts with an id specified in this list
150 * will be run. If the list is null or empty, than all contexts are run.
151 * @throws SQLException
152 * the SQL exception
153 * @throws IOException
154 * Signals that an I/O exception has occurred.
155 * @throws InterruptedException
156 * if the method is canceled through the ProgressCallback
157 */
158 public void generate(ProgressCallback callback, Set<String> contextIds)
159 throws SQLException, IOException, InterruptedException {
160 generate(callback, contextIds, null, true);
161 }
162
163 /**
164 * This is the main method for generating code. This method is long running, but progress can be provided and the
165 * method can be cancelled through the ProgressCallback interface.
166 *
167 * @param callback
168 * an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
169 * information
170 * @param contextIds
171 * a set of Strings containing context ids to run. Only the contexts with an id specified in this list
172 * will be run. If the list is null or empty, than all contexts are run.
173 * @param fullyQualifiedTableNames
174 * a set of table names to generate. The elements of the set must be Strings that exactly match what's
175 * specified in the configuration. For example, if table name = "foo" and schema = "bar", then the fully
176 * qualified table name is "foo.bar". If the Set is null or empty, then all tables in the configuration
177 * will be used for code generation.
178 * @throws SQLException
179 * the SQL exception
180 * @throws IOException
181 * Signals that an I/O exception has occurred.
182 * @throws InterruptedException
183 * if the method is canceled through the ProgressCallback
184 */
185 public void generate(ProgressCallback callback, Set<String> contextIds,
186 Set<String> fullyQualifiedTableNames) throws SQLException,
187 IOException, InterruptedException {
188 generate(callback, contextIds, fullyQualifiedTableNames, true);
189 }
190
191 /**
192 * This is the main method for generating code. This method is long running, but progress can be provided and the
193 * method can be cancelled through the ProgressCallback interface.
194 *
195 * @param callback
196 * an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
197 * information
198 * @param contextIds
199 * a set of Strings containing context ids to run. Only the contexts with an id specified in this list
200 * will be run. If the list is null or empty, than all contexts are run.
201 * @param fullyQualifiedTableNames
202 * a set of table names to generate. The elements of the set must be Strings that exactly match what's
203 * specified in the configuration. For example, if table name = "foo" and schema = "bar", then the fully
204 * qualified table name is "foo.bar". If the Set is null or empty, then all tables in the configuration
205 * will be used for code generation.
206 * @param writeFiles
207 * if true, then the generated files will be written to disk. If false,
208 * then the generator runs but nothing is written
209 * @throws SQLException
210 * the SQL exception
211 * @throws IOException
212 * Signals that an I/O exception has occurred.
213 * @throws InterruptedException
214 * if the method is canceled through the ProgressCallback
215 */
216 public void generate(ProgressCallback callback, Set<String> contextIds,
217 Set<String> fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
218 IOException, InterruptedException {
219
220 if (callback == null) {
221 callback = new NullProgressCallback();
222 }
223
224 generatedJavaFiles.clear();
225 generatedXmlFiles.clear();
226
227 // calculate the contexts to run
228 List<Context> contextsToRun;
229 if (contextIds == null || contextIds.size() == 0) {
230 contextsToRun = configuration.getContexts();
231 } else {
232 contextsToRun = new ArrayList<Context>();
233 for (Context context : configuration.getContexts()) {
234 if (contextIds.contains(context.getId())) {
235 contextsToRun.add(context);
236 }
237 }
238 }
239
240 // setup custom classloader if required
241 if (configuration.getClassPathEntries().size() > 0) {
242 ClassLoader classLoader = getCustomClassloader(configuration.getClassPathEntries());
243 ObjectFactory.addExternalClassLoader(classLoader);
244 }
245
246 // now run the introspections...
247 int totalSteps = 0;
248 for (Context context : contextsToRun) {
249 totalSteps += context.getIntrospectionSteps();
250 }
251 callback.introspectionStarted(totalSteps);
252
253 for (Context context : contextsToRun) {
254 context.introspectTables(callback, warnings,
255 fullyQualifiedTableNames);
256 }
257
258 // now run the generates
259 totalSteps = 0;
260 for (Context context : contextsToRun) {
261 totalSteps += context.getGenerationSteps();
262 }
263 callback.generationStarted(totalSteps);
264
265 for (Context context : contextsToRun) {
266 context.generateFiles(callback, generatedJavaFiles,
267 generatedXmlFiles, warnings);
268 }
269
270 // now save the files
271 if (writeFiles) {
272 callback.saveStarted(generatedXmlFiles.size()
273 + generatedJavaFiles.size());
274
275 for (GeneratedXmlFile gxf : generatedXmlFiles) {
276 projects.add(gxf.getTargetProject());
277 writeGeneratedXmlFile(gxf, callback);
278 }
279
280 for (GeneratedJavaFile gjf : generatedJavaFiles) {
281 projects.add(gjf.getTargetProject());
282 writeGeneratedJavaFile(gjf, callback);
283 }
284
285 for (String project : projects) {
286 shellCallback.refreshProject(project);
287 }
288 }
289
290 callback.done();
291 }
292
293 private void writeGeneratedJavaFile(GeneratedJavaFile gjf, ProgressCallback callback)
294 throws InterruptedException, IOException {
295 File targetFile;
296 String source;
297 try {
298 File directory = shellCallback.getDirectory(gjf
299 .getTargetProject(), gjf.getTargetPackage());
300 targetFile = new File(directory, gjf.getFileName());
301 if (targetFile.exists()) {
302 if (shellCallback.isMergeSupported()) {
303 source = shellCallback.mergeJavaFile(gjf
304 .getFormattedContent(), targetFile
305 .getAbsolutePath(),
306 MergeConstants.OLD_ELEMENT_TAGS,
307 gjf.getFileEncoding());
308 } else if (shellCallback.isOverwriteEnabled()) {
309 source = gjf.getFormattedContent();
310 warnings.add(getString("Warning.11", //$NON-NLS-1$
311 targetFile.getAbsolutePath()));
312 } else {
313 source = gjf.getFormattedContent();
314 targetFile = getUniqueFileName(directory, gjf
315 .getFileName());
316 warnings.add(getString(
317 "Warning.2", targetFile.getAbsolutePath())); //$NON-NLS-1$
318 }
319 } else {
320 source = gjf.getFormattedContent();
321 }
322
323 callback.checkCancel();
324 callback.startTask(getString(
325 "Progress.15", targetFile.getName())); //$NON-NLS-1$
326 writeFile(targetFile, source, gjf.getFileEncoding());
327 } catch (ShellException e) {
328 warnings.add(e.getMessage());
329 }
330 }
331
332 private void writeGeneratedXmlFile(GeneratedXmlFile gxf, ProgressCallback callback)
333 throws InterruptedException, IOException {
334 File targetFile;
335 String source;
336 try {
337 File directory = shellCallback.getDirectory(gxf
338 .getTargetProject(), gxf.getTargetPackage());
339 targetFile = new File(directory, gxf.getFileName());
340 if (targetFile.exists()) {
341 if (gxf.isMergeable()) {
342 source = XmlFileMergerJaxp.getMergedSource(gxf,
343 targetFile);
344 } else if (shellCallback.isOverwriteEnabled()) {
345 source = gxf.getFormattedContent();
346 warnings.add(getString("Warning.11", //$NON-NLS-1$
347 targetFile.getAbsolutePath()));
348 } else {
349 source = gxf.getFormattedContent();
350 targetFile = getUniqueFileName(directory, gxf
351 .getFileName());
352 warnings.add(getString(
353 "Warning.2", targetFile.getAbsolutePath())); //$NON-NLS-1$
354 }
355 } else {
356 source = gxf.getFormattedContent();
357 }
358
359 callback.checkCancel();
360 callback.startTask(getString(
361 "Progress.15", targetFile.getName())); //$NON-NLS-1$
362 writeFile(targetFile, source, "UTF-8"); //$NON-NLS-1$
363 } catch (ShellException e) {
364 warnings.add(e.getMessage());
365 }
366 }
367
368 /**
369 * Writes, or overwrites, the contents of the specified file.
370 *
371 * @param file
372 * the file
373 * @param content
374 * the content
375 * @param fileEncoding
376 * the file encoding
377 * @throws IOException
378 * Signals that an I/O exception has occurred.
379 */
380 private void writeFile(File file, String content, String fileEncoding) throws IOException {
381 FileOutputStream fos = new FileOutputStream(file, false);
382 OutputStreamWriter osw;
383 if (fileEncoding == null) {
384 osw = new OutputStreamWriter(fos);
385 } else {
386 osw = new OutputStreamWriter(fos, fileEncoding);
387 }
388
389 BufferedWriter bw = new BufferedWriter(osw);
390 bw.write(content);
391 bw.close();
392 }
393
394 /**
395 * Gets the unique file name.
396 *
397 * @param directory
398 * the directory
399 * @param fileName
400 * the file name
401 * @return the unique file name
402 */
403 private File getUniqueFileName(File directory, String fileName) {
404 File answer = null;
405
406 // try up to 1000 times to generate a unique file name
407 StringBuilder sb = new StringBuilder();
408 for (int i = 1; i < 1000; i++) {
409 sb.setLength(0);
410 sb.append(fileName);
411 sb.append('.');
412 sb.append(i);
413
414 File testFile = new File(directory, sb.toString());
415 if (!testFile.exists()) {
416 answer = testFile;
417 break;
418 }
419 }
420
421 if (answer == null) {
422 throw new RuntimeException(getString(
423 "RuntimeError.3", directory.getAbsolutePath())); //$NON-NLS-1$
424 }
425
426 return answer;
427 }
428
429 /**
430 * Returns the list of generated Java files after a call to one of the generate methods.
431 * This is useful if you prefer to process the generated files yourself and do not want
432 * the generator to write them to disk.
433 *
434 * @return the list of generated Java files
435 */
436 public List<GeneratedJavaFile> getGeneratedJavaFiles() {
437 return generatedJavaFiles;
438 }
439
440 /**
441 * Returns the list of generated XML files after a call to one of the generate methods.
442 * This is useful if you prefer to process the generated files yourself and do not want
443 * the generator to write them to disk.
444 *
445 * @return the list of generated XML files
446 */
447 public List<GeneratedXmlFile> getGeneratedXmlFiles() {
448 return generatedXmlFiles;
449 }
450 }