View Javadoc
1   /*
2    *    Copyright 2006-2025 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.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.IOException;
24  import java.io.OutputStream;
25  import java.io.OutputStreamWriter;
26  import java.nio.charset.Charset;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.StandardOpenOption;
30  import java.sql.SQLException;
31  import java.util.ArrayList;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Objects;
35  import java.util.Set;
36  
37  import org.mybatis.generator.codegen.RootClassInfo;
38  import org.mybatis.generator.config.Configuration;
39  import org.mybatis.generator.config.Context;
40  import org.mybatis.generator.config.MergeConstants;
41  import org.mybatis.generator.exception.InvalidConfigurationException;
42  import org.mybatis.generator.exception.ShellException;
43  import org.mybatis.generator.internal.DefaultShellCallback;
44  import org.mybatis.generator.internal.ObjectFactory;
45  import org.mybatis.generator.internal.XmlFileMergerJaxp;
46  
47  /**
48   * This class is the main interface to MyBatis generator. A typical execution of the tool involves these steps:
49   * <ol>
50   * <li>Create a Configuration object. The Configuration can be the result of a parsing the XML configuration file, or it
51   * can be created solely in Java.</li>
52   * <li>Create a MyBatisGenerator object</li>
53   * <li>Call one of the generate() methods</li>
54   * </ol>
55   *
56   * @author Jeff Butler
57   *
58   * @see org.mybatis.generator.config.xml.ConfigurationParser
59   */
60  public class MyBatisGenerator {
61  
62      private static final ProgressCallback NULL_PROGRESS_CALLBACK = new ProgressCallback() {
63      };
64  
65      private final Configuration configuration;
66  
67      private final ShellCallback shellCallback;
68  
69      private final List<GeneratedJavaFile> generatedJavaFiles = new ArrayList<>();
70  
71      private final List<GeneratedXmlFile> generatedXmlFiles = new ArrayList<>();
72  
73      private final List<GeneratedKotlinFile> generatedKotlinFiles = new ArrayList<>();
74  
75      /**
76       * Any kind of generated file generated by plugin methods contextGenerateAdditionalFiles.
77       */
78      private final List<GeneratedFile> otherGeneratedFiles = new ArrayList<>();
79  
80      private final List<String> warnings;
81  
82      private final Set<String> projects = new HashSet<>();
83  
84      /**
85       * Constructs a MyBatisGenerator object.
86       *
87       * @param configuration
88       *            The configuration for this invocation
89       * @param shellCallback
90       *            an instance of a ShellCallback interface. You may specify
91       *            <code>null</code> in which case the DefaultShellCallback will
92       *            be used.
93       * @param warnings
94       *            Any warnings generated during execution will be added to this
95       *            list. Warnings do not affect the running of the tool, but they
96       *            may affect the results. A typical warning is an unsupported
97       *            data type. In that case, the column will be ignored and
98       *            generation will continue. You may specify <code>null</code> if
99       *            you do not want warnings returned.
100      * @throws InvalidConfigurationException
101      *             if the specified configuration is invalid
102      */
103     public MyBatisGenerator(Configuration configuration, ShellCallback shellCallback,
104             List<String> warnings) throws InvalidConfigurationException {
105         super();
106         if (configuration == null) {
107             throw new IllegalArgumentException(getString("RuntimeError.2")); //$NON-NLS-1$
108         } else {
109             this.configuration = configuration;
110         }
111 
112         this.shellCallback = Objects.requireNonNullElseGet(shellCallback, () -> new DefaultShellCallback(false));
113 
114         this.warnings = Objects.requireNonNullElseGet(warnings, ArrayList::new);
115 
116         this.configuration.validate();
117     }
118 
119     /**
120      * This is the main method for generating code. This method is long-running, but progress can be provided and the
121      * method can be canceled through the ProgressCallback interface. This version of the method runs all configured
122      * contexts.
123      *
124      * @param callback
125      *            an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
126      *            information
127      * @throws SQLException
128      *             the SQL exception
129      * @throws IOException
130      *             Signals that an I/O exception has occurred.
131      * @throws InterruptedException
132      *             if the method is canceled through the ProgressCallback
133      */
134     public void generate(ProgressCallback callback) throws SQLException,
135             IOException, InterruptedException {
136         generate(callback, null, null, true);
137     }
138 
139     /**
140      * This is the main method for generating code. This method is long-running, but progress can be provided and the
141      * method can be canceled through the ProgressCallback interface.
142      *
143      * @param callback
144      *            an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
145      *            information
146      * @param contextIds
147      *            a set of Strings containing context ids to run. Only the contexts with an id specified in this list
148      *            will be run. If the list is null or empty, then all contexts are run.
149      * @throws SQLException
150      *             the SQL exception
151      * @throws IOException
152      *             Signals that an I/O exception has occurred.
153      * @throws InterruptedException
154      *             if the method is canceled through the ProgressCallback
155      */
156     public void generate(ProgressCallback callback, Set<String> contextIds)
157             throws SQLException, IOException, InterruptedException {
158         generate(callback, contextIds, null, true);
159     }
160 
161     /**
162      * This is the main method for generating code. This method is long-running, but progress can be provided and the
163      * method can be cancelled through the ProgressCallback interface.
164      *
165      * @param callback
166      *            an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
167      *            information
168      * @param contextIds
169      *            a set of Strings containing context ids to run. Only the contexts with an id specified in this list
170      *            will be run. If the list is null or empty, then all contexts are run.
171      * @param fullyQualifiedTableNames
172      *            a set of table names to generate. The elements of the set must be Strings that exactly match what's
173      *            specified in the configuration. For example, if table name = "foo" and schema = "bar", then the fully
174      *            qualified table name is "foo.bar". If the Set is null or empty, then all tables in the configuration
175      *            will be used for code generation.
176      * @throws SQLException
177      *             the SQL exception
178      * @throws IOException
179      *             Signals that an I/O exception has occurred.
180      * @throws InterruptedException
181      *             if the method is canceled through the ProgressCallback
182      */
183     public void generate(ProgressCallback callback, Set<String> contextIds,
184             Set<String> fullyQualifiedTableNames) throws SQLException,
185             IOException, InterruptedException {
186         generate(callback, contextIds, fullyQualifiedTableNames, true);
187     }
188 
189     /**
190      * This is the main method for generating code. This method is long-running, but progress can be provided and the
191      * method can be cancelled through the ProgressCallback interface.
192      *
193      * @param callback
194      *            an instance of the ProgressCallback interface, or <code>null</code> if you do not require progress
195      *            information
196      * @param contextIds
197      *            a set of Strings containing context ids to run. Only the contexts with an id specified in this list
198      *            will be run. If the list is null or empty, then all contexts are run.
199      * @param fullyQualifiedTableNames
200      *            a set of table names to generate. The elements of the set must be Strings that exactly match what's
201      *            specified in the configuration. For example, if table name = "foo" and schema = "bar", then the fully
202      *            qualified table name is "foo.bar". If the Set is null or empty, then all tables in the configuration
203      *            will be used for code generation.
204      * @param writeFiles
205      *            if true, then the generated files will be written to disk.  If false,
206      *            then the generator runs but nothing is written
207      * @throws SQLException
208      *             the SQL exception
209      * @throws IOException
210      *             Signals that an I/O exception has occurred.
211      * @throws InterruptedException
212      *             if the method is canceled through the ProgressCallback
213      */
214     public void generate(ProgressCallback callback, Set<String> contextIds,
215             Set<String> fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
216             IOException, InterruptedException {
217 
218         if (callback == null) {
219             callback = NULL_PROGRESS_CALLBACK;
220         }
221 
222         generatedJavaFiles.clear();
223         generatedXmlFiles.clear();
224         ObjectFactory.reset();
225         RootClassInfo.reset();
226 
227         // calculate the contexts to run
228         List<Context> contextsToRun;
229         if (contextIds == null || contextIds.isEmpty()) {
230             contextsToRun = configuration.getContexts();
231         } else {
232             contextsToRun = new ArrayList<>();
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().isEmpty()) {
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, generatedKotlinFiles, otherGeneratedFiles, 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 (GeneratedKotlinFile gkf : generatedKotlinFiles) {
286                 projects.add(gkf.getTargetProject());
287                 writeGeneratedFile(gkf, callback);
288             }
289 
290             for (GeneratedFile gf : otherGeneratedFiles) {
291                 projects.add(gf.getTargetProject());
292                 writeGeneratedFile(gf, callback);
293             }
294 
295             for (String project : projects) {
296                 shellCallback.refreshProject(project);
297             }
298         }
299 
300         callback.done();
301     }
302 
303     private void writeGeneratedJavaFile(GeneratedJavaFile gjf, ProgressCallback callback)
304             throws InterruptedException, IOException {
305         Path targetFile;
306         String source;
307         try {
308             File directory = shellCallback.getDirectory(gjf
309                     .getTargetProject(), gjf.getTargetPackage());
310             targetFile = directory.toPath().resolve(gjf.getFileName());
311             if (Files.exists(targetFile)) {
312                 if (shellCallback.isMergeSupported()) {
313                     source = shellCallback.mergeJavaFile(gjf
314                             .getFormattedContent(), targetFile.toFile(),
315                             MergeConstants.getOldElementTags(),
316                             gjf.getFileEncoding());
317                 } else if (shellCallback.isOverwriteEnabled()) {
318                     source = gjf.getFormattedContent();
319                     warnings.add(getString("Warning.11", //$NON-NLS-1$
320                             targetFile.toFile().getAbsolutePath()));
321                 } else {
322                     source = gjf.getFormattedContent();
323                     targetFile = getUniqueFileName(directory, gjf
324                             .getFileName());
325                     warnings.add(getString(
326                             "Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
327                 }
328             } else {
329                 source = gjf.getFormattedContent();
330             }
331 
332             callback.checkCancel();
333             callback.startTask(getString(
334                     "Progress.15", targetFile.toString())); //$NON-NLS-1$
335             writeFile(targetFile.toFile(), source, gjf.getFileEncoding());
336         } catch (ShellException e) {
337             warnings.add(e.getMessage());
338         }
339     }
340 
341     private void writeGeneratedFile(GeneratedFile gf, ProgressCallback callback)
342             throws InterruptedException, IOException {
343         Path targetFile;
344         String source;
345         try {
346             File directory = shellCallback.getDirectory(gf
347                     .getTargetProject(), gf.getTargetPackage());
348             targetFile = directory.toPath().resolve(gf.getFileName());
349             if (Files.exists(targetFile)) {
350                 if (shellCallback.isOverwriteEnabled()) {
351                     source = gf.getFormattedContent();
352                     warnings.add(getString("Warning.11", //$NON-NLS-1$
353                             targetFile.toFile().getAbsolutePath()));
354                 } else {
355                     source = gf.getFormattedContent();
356                     targetFile = getUniqueFileName(directory, gf
357                             .getFileName());
358                     warnings.add(getString(
359                             "Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
360                 }
361             } else {
362                 source = gf.getFormattedContent();
363             }
364 
365             callback.checkCancel();
366             callback.startTask(getString(
367                     "Progress.15", targetFile.toString())); //$NON-NLS-1$
368             writeFile(targetFile.toFile(), source, gf.getFileEncoding());
369         } catch (ShellException e) {
370             warnings.add(e.getMessage());
371         }
372     }
373 
374     private void writeGeneratedXmlFile(GeneratedXmlFile gxf, ProgressCallback callback)
375             throws InterruptedException, IOException {
376         Path targetFile;
377         String source;
378         try {
379             File directory = shellCallback.getDirectory(gxf
380                     .getTargetProject(), gxf.getTargetPackage());
381             targetFile = directory.toPath().resolve(gxf.getFileName());
382             if (Files.exists(targetFile)) {
383                 if (gxf.isMergeable()) {
384                     source = XmlFileMergerJaxp.getMergedSource(gxf,
385                             targetFile.toFile());
386                 } else if (shellCallback.isOverwriteEnabled()) {
387                     source = gxf.getFormattedContent();
388                     warnings.add(getString("Warning.11", //$NON-NLS-1$
389                             targetFile.toFile().getAbsolutePath()));
390                 } else {
391                     source = gxf.getFormattedContent();
392                     targetFile = getUniqueFileName(directory, gxf
393                             .getFileName());
394                     warnings.add(getString(
395                             "Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
396                 }
397             } else {
398                 source = gxf.getFormattedContent();
399             }
400 
401             callback.checkCancel();
402             callback.startTask(getString(
403                     "Progress.15", targetFile.toString())); //$NON-NLS-1$
404             writeFile(targetFile.toFile(), source, gxf.getFileEncoding());
405         } catch (ShellException e) {
406             warnings.add(e.getMessage());
407         }
408     }
409 
410     /**
411      * Writes, or overwrites, the contents of the specified file.
412      *
413      * @param file
414      *            the file
415      * @param content
416      *            the content
417      * @param fileEncoding
418      *            the file encoding
419      * @throws IOException
420      *             Signals that an I/O exception has occurred.
421      */
422     private void writeFile(File file, String content, String fileEncoding) throws IOException {
423         try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
424             OutputStreamWriter osw;
425             if (fileEncoding == null) {
426                 osw = new OutputStreamWriter(fos);
427             } else {
428                 osw = new OutputStreamWriter(fos, Charset.forName(fileEncoding));
429             }
430 
431             try (BufferedWriter bw = new BufferedWriter(osw)) {
432                 bw.write(content);
433             }
434         }
435     }
436 
437     /**
438      * Gets the unique file name.
439      *
440      * @param directory
441      *            the directory
442      * @param fileName
443      *            the file name
444      * @return the unique file name
445      */
446     private Path getUniqueFileName(File directory, String fileName) {
447         Path answer = null;
448 
449         // try up to 1000 times to generate a unique file name
450         StringBuilder sb = new StringBuilder();
451         for (int i = 1; i < 1000; i++) {
452             sb.setLength(0);
453             sb.append(fileName);
454             sb.append('.');
455             sb.append(i);
456 
457             Path testFile = directory.toPath().resolve(sb.toString());
458             if (Files.notExists(testFile)) {
459                 answer = testFile;
460                 break;
461             }
462         }
463 
464         if (answer == null) {
465             throw new RuntimeException(getString(
466                     "RuntimeError.3", directory.getAbsolutePath())); //$NON-NLS-1$
467         }
468 
469         return answer;
470     }
471 
472     /**
473      * Returns the list of generated Java files after a call to one of the generate methods.
474      * This is useful if you prefer to process the generated files yourself and do not want
475      * the generator to write them to disk.
476      *
477      * @return the list of generated Java files
478      */
479     public List<GeneratedJavaFile> getGeneratedJavaFiles() {
480         return generatedJavaFiles;
481     }
482 
483     /**
484      * Returns the list of generated Kotlin files after a call to one of the generate methods.
485      * This is useful if you prefer to process the generated files yourself and do not want
486      * the generator to write them to disk.
487      *
488      * @return the list of generated Kotlin files
489      */
490     public List<GeneratedKotlinFile> getGeneratedKotlinFiles() {
491         return generatedKotlinFiles;
492     }
493 
494     /**
495      * Returns the list of generated XML files after a call to one of the generate methods.
496      * This is useful if you prefer to process the generated files yourself and do not want
497      * the generator to write them to disk.
498      *
499      * @return the list of generated XML files
500      */
501     public List<GeneratedXmlFile> getGeneratedXmlFiles() {
502         return generatedXmlFiles;
503     }
504 }