View Javadoc
1   /*
2    *    Copyright 2006-2026 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.StringUtility.mapStringValueOrElseGet;
20  import static org.mybatis.generator.internal.util.messages.Messages.getString;
21  
22  import java.io.BufferedWriter;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.OutputStream;
26  import java.io.OutputStreamWriter;
27  import java.nio.charset.Charset;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.nio.file.StandardOpenOption;
31  import java.sql.SQLException;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Objects;
37  import java.util.Set;
38  
39  import org.jspecify.annotations.Nullable;
40  import org.mybatis.generator.codegen.CalculatedContextValues;
41  import org.mybatis.generator.codegen.GenerationEngine;
42  import org.mybatis.generator.codegen.GenerationResults;
43  import org.mybatis.generator.codegen.IntrospectionEngine;
44  import org.mybatis.generator.codegen.RootClassInfo;
45  import org.mybatis.generator.config.Configuration;
46  import org.mybatis.generator.config.Context;
47  import org.mybatis.generator.exception.InternalException;
48  import org.mybatis.generator.exception.InvalidConfigurationException;
49  import org.mybatis.generator.exception.MergeException;
50  import org.mybatis.generator.exception.ShellException;
51  import org.mybatis.generator.internal.DefaultShellCallback;
52  import org.mybatis.generator.internal.ObjectFactory;
53  import org.mybatis.generator.merge.java.JavaFileMerger;
54  import org.mybatis.generator.merge.java.JavaMergerFactory;
55  import org.mybatis.generator.merge.xml.XmlFileMergerJaxp;
56  
57  /**
58   * This class is the main interface to MyBatis generator. A typical execution of the tool involves these steps:
59   * <ol>
60   * <li>Create a Configuration object. The Configuration can be the result of a parsing the XML configuration file, or it
61   * can be created solely in Java.</li>
62   * <li>Create a MyBatisGenerator object</li>
63   * <li>Call one of the generate() methods</li>
64   * </ol>
65   *
66   * @author Jeff Butler
67   *
68   * @see org.mybatis.generator.config.xml.ConfigurationParser
69   */
70  public class MyBatisGenerator {
71      private final Configuration configuration;
72      private final ShellCallback shellCallback;
73      private final ProgressCallback progressCallback;
74      private final Set<String> contextIds;
75      private final Set<String> fullyQualifiedTableNames;
76      private final JavaFileMerger javaFileMerger;
77      private final boolean isOverwriteEnabled;
78      private final boolean isJavaFileMergeEnabled;
79  
80      private final List<GenerationResults> generationResultsList = new ArrayList<>();
81  
82      private MyBatisGenerator(Builder builder) {
83          configuration = Objects.requireNonNull(builder.configuration, getString("RuntimeError.2")); //$NON-NLS-1$
84          shellCallback = Objects.requireNonNullElseGet(builder.shellCallback, DefaultShellCallback::new);
85          progressCallback = Objects.requireNonNullElseGet(builder.progressCallback, () -> new ProgressCallback() {});
86          fullyQualifiedTableNames = builder.fullyQualifiedTableNames;
87          contextIds = builder.contextIds;
88  
89          if (builder.isJavaFileMergeEnabled) {
90              isJavaFileMergeEnabled = true;
91              javaFileMerger = JavaMergerFactory.getMerger(JavaMergerFactory.PrinterConfiguration.LEXICAL_PRESERVING);
92          } else {
93              isJavaFileMergeEnabled = false;
94              javaFileMerger = (newContent, existingContent) -> newContent;
95          }
96  
97          isOverwriteEnabled = builder.isOverwriteEnabled;
98      }
99  
100     /**
101      * This is one of the main methods for generating code. This method is long-running, but progress can be provided
102      * and the method can be canceled through the ProgressCallback interface. This method will not write results to
103      * the disk. The generated objects can be retrieved from the getGeneratedJavaFiles(), getGeneratedKotlinFiles(),
104      * getGeneratedXmlFiles(), and getGeneratedGenericFiles() methods.
105      *
106      * @return any warnings created during the generation process
107      * @throws SQLException
108      *             the SQL exception
109      * @throws InterruptedException
110      *             if the method is canceled through the ProgressCallback
111      * @throws InvalidConfigurationException
112      *             if the specified configuration is invalid
113      */
114     public List<String> generateOnly() throws SQLException, InterruptedException, InvalidConfigurationException {
115         List<String> warnings = new ArrayList<>();
116         generateFiles(warnings);
117         progressCallback.done();
118         return warnings;
119     }
120 
121     /**
122      * This is one of the main methods for generating code. This method is long-running, but progress can be provided
123      * and the method can be canceled through the ProgressCallback interface. This method will write results to
124      * the disk.
125      *
126      * @return any warnings created during the generation process
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      * @throws InvalidConfigurationException
134      *             if the specified configuration is invalid
135      */
136     public List<String> generateAndWrite() throws SQLException, IOException, InterruptedException,
137             InvalidConfigurationException {
138         List<String> warnings = new ArrayList<>();
139         generateFiles(warnings);
140         writeGeneratedFiles(warnings);
141         progressCallback.done();
142         return warnings;
143     }
144 
145     private void generateFiles(List<String> warnings) throws SQLException, InterruptedException,
146             InvalidConfigurationException {
147         configuration.validate();
148         generationResultsList.clear();
149         ObjectFactory.reset();
150         RootClassInfo.reset();
151 
152         setupCustomClassloader();
153         List<Context> contextsToRun = calculateContextsToRun();
154         List<CalculatedContextValues> contextValuesList = calculateContextValues(contextsToRun, warnings);
155         List<ContextValuesAndTables> contextValuesAndTablesList = runAllIntrospections(contextValuesList, warnings);
156         List<GenerationEngine> generationEngines = createGenerationEngines(contextValuesAndTablesList, warnings);
157         runGenerationEngines(generationEngines);
158     }
159 
160     private void setupCustomClassloader() {
161         if (!configuration.getClassPathEntries().isEmpty()) {
162             ClassLoader classLoader = getCustomClassloader(configuration.getClassPathEntries());
163             ObjectFactory.addExternalClassLoader(classLoader);
164         }
165     }
166 
167     private List<Context> calculateContextsToRun() {
168         List<Context> contextsToRun;
169         if (fullyQualifiedTableNames.isEmpty()) {
170             contextsToRun = configuration.getContexts();
171         } else {
172             contextsToRun = configuration.getContexts().stream()
173                     .filter(c -> contextIds.contains(c.getId()))
174                     .toList();
175         }
176 
177         return contextsToRun;
178     }
179 
180     private List<CalculatedContextValues> calculateContextValues(List<Context> contextsToRun, List<String> warnings) {
181         return contextsToRun.stream()
182                 .map(c -> createContextValues(c, warnings))
183                 .toList();
184     }
185 
186     private CalculatedContextValues createContextValues(Context context, List<String> warnings) {
187         return new CalculatedContextValues.Builder()
188                 .withContext(context)
189                 .withWarnings(warnings)
190                 .build();
191     }
192 
193     private List<ContextValuesAndTables> runAllIntrospections(List<CalculatedContextValues> contextValuesList,
194                                                               List<String> warnings)
195             throws SQLException, InterruptedException {
196         int totalSteps = contextValuesList.stream()
197                 .map(CalculatedContextValues::context)
198                 .mapToInt(Context::getIntrospectionSteps)
199                 .sum();
200         progressCallback.introspectionStarted(totalSteps);
201 
202         List<ContextValuesAndTables> contextValuesAndTablesList = new ArrayList<>();
203         for (CalculatedContextValues contextValues : contextValuesList) {
204             contextValuesAndTablesList.add(new ContextValuesAndTables(contextValues,
205                     runContextIntrospection(fullyQualifiedTableNames, contextValues, warnings)));
206         }
207 
208         return contextValuesAndTablesList;
209     }
210 
211     private List<IntrospectedTable> runContextIntrospection(Set<String> fullyQualifiedTableNames,
212                                                             CalculatedContextValues contextValues,
213                                                             List<String> warnings)
214             throws SQLException, InterruptedException {
215         return new IntrospectionEngine.Builder()
216                 .withContextValues(contextValues)
217                 .withFullyQualifiedTableNames(fullyQualifiedTableNames)
218                 .withWarnings(warnings)
219                 .withProgressCallback(progressCallback)
220                 .build()
221                 .introspectTables();
222     }
223 
224     private List<GenerationEngine> createGenerationEngines(List<ContextValuesAndTables> contextValuesAndTablesListList,
225                                                            List<String> warnings) {
226         return contextValuesAndTablesListList.stream()
227                 .map(c -> createGenerationEngine(c, warnings))
228                 .toList();
229     }
230 
231     private GenerationEngine createGenerationEngine(ContextValuesAndTables contextValuesAndTables,
232                                                     List<String> warnings) {
233         return new GenerationEngine.Builder()
234                 .withContextValues(contextValuesAndTables.contextValues())
235                 .withProgressCallback(progressCallback)
236                 .withWarnings(warnings)
237                 .withIntrospectedTables(contextValuesAndTables.introspectedTables())
238                 .build();
239     }
240 
241     private void runGenerationEngines(List<GenerationEngine> generationEngines) throws InterruptedException {
242         // calculate the number of steps
243         int totalSteps = generationEngines.stream().mapToInt(GenerationEngine::getGenerationSteps).sum();
244         progressCallback.generationStarted(totalSteps);
245 
246         // now run the generators
247         for (GenerationEngine generationEngine: generationEngines) {
248             var generationResults = generationEngine.generate();
249             generationResultsList.add(generationResults);
250         }
251     }
252 
253     private void writeGeneratedFiles(List<String> warnings) throws IOException, InterruptedException {
254         Set<String> projects = new HashSet<>();
255         int totalSteps = generationResultsList.stream().mapToInt(GenerationResults::getNumberOfGeneratedFiles).sum();
256         progressCallback.saveStarted(totalSteps);
257 
258         for (GenerationResults generationResults : generationResultsList) {
259             for (GeneratedXmlFile gxf : generationResults.generatedXmlFiles()) {
260                 projects.add(gxf.getTargetProject());
261                 writeGeneratedXmlFile(gxf, generationResults.xmlFormatter(), warnings);
262             }
263 
264             for (GeneratedJavaFile gjf : generationResults.generatedJavaFiles()) {
265                 projects.add(gjf.getTargetProject());
266                 writeGeneratedJavaFile(gjf, generationResults.javaFormatter(), generationResults.javaFileEncoding(),
267                         warnings);
268             }
269 
270             for (GeneratedKotlinFile gkf : generationResults.generatedKotlinFiles()) {
271                 projects.add(gkf.getTargetProject());
272                 writeGeneratedKotlinFile(gkf, generationResults.kotlinFormatter(),
273                         generationResults.kotlinFileEncoding(), warnings);
274             }
275 
276             for (GenericGeneratedFile gf : generationResults.generatedGenericFiles()) {
277                 projects.add(gf.getTargetProject());
278                 writeGenericGeneratedFile(gf, warnings);
279             }
280         }
281 
282         for (String project : projects) {
283             shellCallback.refreshProject(project);
284         }
285     }
286 
287     private void writeGeneratedJavaFile(GeneratedJavaFile gf, JavaFormatter javaFormatter,
288                                         @Nullable String javaFileEncoding, List<String> warnings)
289             throws InterruptedException, IOException {
290         String source = javaFormatter.getFormattedContent(gf.getCompilationUnit());
291         writeFile(source, javaFileEncoding, gf, warnings, isJavaFileMergeEnabled,
292                 (newContent, existingContent) -> javaFileMerger.getMergedSource(newContent, existingContent,
293                         javaFileEncoding));
294     }
295 
296     private void writeGeneratedKotlinFile(GeneratedKotlinFile gf, KotlinFormatter kotlinFormatter,
297                                           @Nullable String kotlinFileEncoding, List<String> warnings)
298             throws InterruptedException, IOException {
299         String source = kotlinFormatter.getFormattedContent(gf.getKotlinFile());
300         writeFile(source, kotlinFileEncoding, gf, warnings, false, Merger.noMerge());
301     }
302 
303     private void writeGenericGeneratedFile(GenericGeneratedFile gf, List<String> warnings)
304             throws InterruptedException, IOException {
305         String source = gf.getFormattedContent();
306         writeFile(source, gf.getFileEncoding().orElse(null), gf, warnings, false, Merger.noMerge());
307     }
308 
309     private void writeGeneratedXmlFile(GeneratedXmlFile gf, XmlFormatter xmlFormatter, List<String> warnings)
310             throws InterruptedException, IOException {
311         String source = xmlFormatter.getFormattedContent(gf.getDocument());
312         writeFile(source, "UTF-8", gf, warnings, true, XmlFileMergerJaxp::getMergedSource); //$NON-NLS-1$
313     }
314 
315     private void writeFile(String content, @Nullable String encoding, GeneratedFile gf, List<String> warnings,
316                            boolean mergeEnabled, Merger merger)
317             throws InterruptedException, IOException {
318         try {
319             File directory = shellCallback.getDirectory(gf.getTargetProject(), gf.getTargetPackage());
320             Path targetFile = directory.toPath().resolve(gf.getFileName());
321             if (Files.exists(targetFile)) {
322                 if (mergeEnabled && gf.isMergeable()) {
323                     content = merger.apply(content, targetFile.toFile());
324                 } else if (isOverwriteEnabled) {
325                     warnings.add(getString("Warning.11", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
326                 } else {
327                     targetFile = getUniqueFileName(directory, gf.getFileName());
328                     warnings.add(getString("Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
329                 }
330             }
331 
332             progressCallback.checkCancel();
333             progressCallback.startTask(getString("Progress.15", targetFile.toString())); //$NON-NLS-1$
334             writeFile(targetFile.toFile(), content, encoding);
335         } catch (ShellException e) {
336             warnings.add(e.getMessage());
337         } catch (MergeException e) {
338             warnings.add(e.getMessage());
339             warnings.addAll(e.getExtraMessages());
340         }
341     }
342 
343     /**
344      * Writes, or overwrites, the contents of the specified file.
345      *
346      * @param file
347      *            the file
348      * @param content
349      *            the content
350      * @param fileEncoding
351      *            the file encoding
352      * @throws IOException
353      *             Signals that an I/O exception has occurred.
354      */
355     private void writeFile(File file, String content, @Nullable String fileEncoding) throws IOException {
356         Charset cs = mapStringValueOrElseGet(fileEncoding, Charset::forName, Charset::defaultCharset);
357         try (OutputStream outputStream = Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE,
358                 StandardOpenOption.TRUNCATE_EXISTING)) {
359             try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, cs)) {
360                 try (BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter)) {
361                     bufferedWriter.write(content);
362                 }
363             }
364         }
365     }
366 
367     /**
368      * Gets the unique file name.
369      *
370      * @param directory
371      *            the directory
372      * @param fileName
373      *            the file name
374      * @return the unique file name
375      */
376     private Path getUniqueFileName(File directory, String fileName) {
377         Path answer = null;
378 
379         // try up to 1000 times to generate a unique file name
380         StringBuilder sb = new StringBuilder();
381         for (int i = 1; i < 1000; i++) {
382             sb.setLength(0);
383             sb.append(fileName);
384             sb.append('.');
385             sb.append(i);
386 
387             Path testFile = directory.toPath().resolve(sb.toString());
388             if (Files.notExists(testFile)) {
389                 answer = testFile;
390                 break;
391             }
392         }
393 
394         if (answer == null) {
395             throw new InternalException(getString("RuntimeError.3", directory.getAbsolutePath())); //$NON-NLS-1$
396         }
397 
398         return answer;
399     }
400 
401     /**
402      * Returns the list of generated Java files after a call to one of the generate methods.
403      * This is useful if you prefer to process the generated files yourself and do not want
404      * the generator to write them to disk.
405      *
406      * @return the list of generated Java files
407      */
408     public List<GeneratedJavaFile> getGeneratedJavaFiles() {
409         return generationResultsList.stream()
410                 .map(GenerationResults::generatedJavaFiles)
411                 .flatMap(Collection::stream)
412                 .toList();
413     }
414 
415     /**
416      * Returns the list of generated Kotlin files after a call to one of the generate methods.
417      * This is useful if you prefer to process the generated files yourself and do not want
418      * the generator to write them to disk.
419      *
420      * @return the list of generated Kotlin files
421      */
422     public List<GeneratedKotlinFile> getGeneratedKotlinFiles() {
423         return generationResultsList.stream()
424                 .map(GenerationResults::generatedKotlinFiles)
425                 .flatMap(Collection::stream)
426                 .toList();
427     }
428 
429     /**
430      * Returns the list of generated XML 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 XML files
435      */
436     public List<GeneratedXmlFile> getGeneratedXmlFiles() {
437         return generationResultsList.stream()
438                 .map(GenerationResults::generatedXmlFiles)
439                 .flatMap(Collection::stream)
440                 .toList();
441     }
442 
443     /**
444      * Returns the list of generated generic files after a call to one of the generate methods.
445      * This is useful if you prefer to process the generated files yourself and do not want
446      * the generator to write them to disk.
447      *
448      * <p>The list will be empty unless you have used a plugin that generates generic files
449      * or are using a custom runtime.
450      *
451      * @return the list of generated generic files
452      */
453     public List<GenericGeneratedFile> getGeneratedGenericFiles() {
454         return generationResultsList.stream()
455                 .map(GenerationResults::generatedGenericFiles)
456                 .flatMap(Collection::stream)
457                 .toList();
458     }
459 
460     private record ContextValuesAndTables(CalculatedContextValues contextValues,
461                                           List<IntrospectedTable> introspectedTables) { }
462 
463     @FunctionalInterface
464     private interface Merger {
465         String apply(String newContent, File existingContent) throws MergeException;
466 
467         static Merger noMerge() {
468             return (newContent, existingContent) -> newContent;
469         }
470     }
471 
472     public static class Builder {
473         private @Nullable Configuration configuration;
474         private @Nullable ShellCallback shellCallback;
475         private @Nullable ProgressCallback progressCallback;
476         private final Set<String> contextIds = new HashSet<>();
477         private final Set<String> fullyQualifiedTableNames = new HashSet<>();
478         private boolean isOverwriteEnabled = false;
479         private boolean isJavaFileMergeEnabled = false;
480 
481         public Builder withConfiguration(Configuration configuration) {
482             this.configuration = configuration;
483             return this;
484         }
485 
486         public Builder withShellCallback(ShellCallback shellCallback) {
487             this.shellCallback = shellCallback;
488             return this;
489         }
490 
491         public Builder withProgressCallback(@Nullable ProgressCallback progressCallback) {
492             this.progressCallback = progressCallback;
493             return this;
494         }
495 
496         /**
497          * Set of context IDs to use in generation. Only the contexts with an id specified in this set will run.
498          * If the set is empty, then all contexts are run.
499          *
500          * @param contextIds
501          *            a set of contextIds to use in code generation
502          *
503          * @return this builder
504          */
505         public Builder withContextIds(Set<String> contextIds) {
506             this.contextIds.addAll(contextIds);
507             return this;
508         }
509 
510         /**
511          *  Set of table names to generate. The elements of the set must be Strings that exactly match what's
512          *  specified in the configuration. For example, if a table name = "foo" and schema = "bar", then the fully
513          *  qualified table name is "foo.bar". If the Set is empty, then all tables in the configuration
514          *  will be used for code generation.
515          *
516          * @param fullyQualifiedTableNames
517          *            a set of table names to use in code generation
518          *
519          * @return this builder
520          */
521         public Builder withFullyQualifiedTableNames(Set<String> fullyQualifiedTableNames) {
522             this.fullyQualifiedTableNames.addAll(fullyQualifiedTableNames);
523             return this;
524         }
525 
526         /**
527          * If true, then newly generated files will overwrite existing files if there is a collision.
528          * If false, then newly generated files will be written with a unique name when there is a collision.
529          *
530          * <p>The default is <code>false</code></p>
531          *
532          * @param overwriteEnabled where newly generated files should overwrite existing files if there is a collision
533          * @return this builder
534          */
535         public Builder withOverwriteEnabled(boolean overwriteEnabled) {
536             this.isOverwriteEnabled = overwriteEnabled;
537             return this;
538         }
539 
540         /**
541          * If true, then newly generated Java files will be merged if they collide with existing files.
542          * If false, then the {@link #withOverwriteEnabled(boolean)} value governs what happens on a collision.
543          *
544          * <p>The default is <code>false</code></p>
545          *
546          * @param javaFileMergeEnabled where the Java file merger support should be enabled
547          * @return this builder
548          */
549         public Builder withJavaFileMergeEnabled(boolean javaFileMergeEnabled) {
550             this.isJavaFileMergeEnabled = javaFileMergeEnabled;
551             return this;
552         }
553 
554         public MyBatisGenerator build() {
555             return new MyBatisGenerator(this);
556         }
557     }
558 }