• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / generator / 1941

12 Jan 2026 04:39PM UTC coverage: 88.75% (+0.4%) from 88.365%
1941

Pull #1412

github

web-flow
Merge b9b8be318 into 98a4124a3
Pull Request #1412: Adopt JSpecify

2331 of 3162 branches covered (73.72%)

1800 of 1949 new or added lines in 202 files covered. (92.36%)

18 existing lines in 10 files now uncovered.

11384 of 12827 relevant lines covered (88.75%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

25.14
/core/mybatis-generator-core/src/main/java/org/mybatis/generator/api/MyBatisGenerator.java
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.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.jspecify.annotations.Nullable;
38
import org.mybatis.generator.codegen.RootClassInfo;
39
import org.mybatis.generator.config.Configuration;
40
import org.mybatis.generator.config.Context;
41
import org.mybatis.generator.config.MergeConstants;
42
import org.mybatis.generator.exception.InvalidConfigurationException;
43
import org.mybatis.generator.exception.ShellException;
44
import org.mybatis.generator.internal.DefaultShellCallback;
45
import org.mybatis.generator.internal.ObjectFactory;
46
import org.mybatis.generator.internal.XmlFileMergerJaxp;
47

48
/**
49
 * This class is the main interface to MyBatis generator. A typical execution of the tool involves these steps:
50
 * <ol>
51
 * <li>Create a Configuration object. The Configuration can be the result of a parsing the XML configuration file, or it
52
 * can be created solely in Java.</li>
53
 * <li>Create a MyBatisGenerator object</li>
54
 * <li>Call one of the generate() methods</li>
55
 * </ol>
56
 *
57
 * @author Jeff Butler
58
 *
59
 * @see org.mybatis.generator.config.xml.ConfigurationParser
60
 */
61
public class MyBatisGenerator {
62

63
    private static final ProgressCallback NULL_PROGRESS_CALLBACK = new ProgressCallback() {
1✔
64
    };
65

66
    private final Configuration configuration;
67

68
    private final ShellCallback shellCallback;
69

70
    private final List<GeneratedJavaFile> generatedJavaFiles = new ArrayList<>();
1✔
71

72
    private final List<GeneratedXmlFile> generatedXmlFiles = new ArrayList<>();
1✔
73

74
    private final List<GeneratedKotlinFile> generatedKotlinFiles = new ArrayList<>();
1✔
75

76
    /**
77
     * Any kind of generated file generated by plugin methods contextGenerateAdditionalFiles.
78
     */
79
    private final List<GeneratedFile> otherGeneratedFiles = new ArrayList<>();
1✔
80

81
    private final List<String> warnings;
82

83
    private final Set<String> projects = new HashSet<>();
1✔
84

85
    /**
86
     * Constructs a MyBatisGenerator object.
87
     *
88
     * @param configuration
89
     *            The configuration for this invocation
90
     * @param shellCallback
91
     *            an instance of a ShellCallback interface. You may specify
92
     *            <code>null</code> in which case the DefaultShellCallback will
93
     *            be used.
94
     * @param warnings
95
     *            Any warnings generated during execution will be added to this
96
     *            list. Warnings do not affect the running of the tool, but they
97
     *            may affect the results. A typical warning is an unsupported
98
     *            data type. In that case, the column will be ignored and
99
     *            generation will continue. You may specify <code>null</code> if
100
     *            you do not want warnings returned.
101
     * @throws InvalidConfigurationException
102
     *             if the specified configuration is invalid
103
     */
104
    public MyBatisGenerator(Configuration configuration, @Nullable ShellCallback shellCallback,
105
            @Nullable List<String> warnings) throws InvalidConfigurationException {
1✔
106
        if (configuration == null) {
1!
107
            throw new IllegalArgumentException(getString("RuntimeError.2")); //$NON-NLS-1$
×
108
        } else {
109
            this.configuration = configuration;
1✔
110
        }
111

112
        this.shellCallback = Objects.requireNonNullElseGet(shellCallback, () -> new DefaultShellCallback(false));
1✔
113

114
        this.warnings = Objects.requireNonNullElseGet(warnings, ArrayList::new);
1✔
115

116
        this.configuration.validate();
1✔
117
    }
1✔
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(@Nullable ProgressCallback callback, @Nullable Set<String> contextIds,
184
            @Nullable 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(@Nullable ProgressCallback callback, @Nullable Set<String> contextIds,
215
                         @Nullable Set<String> fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
216
            IOException, InterruptedException {
217

218
        if (callback == null) {
1!
219
            callback = NULL_PROGRESS_CALLBACK;
1✔
220
        }
221

222
        generatedJavaFiles.clear();
1✔
223
        generatedXmlFiles.clear();
1✔
224
        ObjectFactory.reset();
1✔
225
        RootClassInfo.reset();
1✔
226

227
        // calculate the contexts to run
228
        List<Context> contextsToRun;
229
        if (contextIds == null || contextIds.isEmpty()) {
1!
230
            contextsToRun = configuration.getContexts();
1✔
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()) {
1!
242
            ClassLoader classLoader = getCustomClassloader(configuration.getClassPathEntries());
×
243
            ObjectFactory.addExternalClassLoader(classLoader);
×
244
        }
245

246
        // now run the introspections...
247
        int totalSteps = 0;
1✔
248
        for (Context context : contextsToRun) {
1✔
249
            totalSteps += context.getIntrospectionSteps();
1✔
250
        }
1✔
251
        callback.introspectionStarted(totalSteps);
1✔
252

253
        for (Context context : contextsToRun) {
1✔
254
            context.introspectTables(callback, warnings, fullyQualifiedTableNames);
1✔
255
        }
1✔
256

257
        // now run the generates
258
        totalSteps = 0;
1✔
259
        for (Context context : contextsToRun) {
1✔
260
            totalSteps += context.getGenerationSteps();
1✔
261
        }
1✔
262
        callback.generationStarted(totalSteps);
1✔
263

264
        for (Context context : contextsToRun) {
1✔
265
            context.generateFiles(callback, generatedJavaFiles,
1✔
266
                    generatedXmlFiles, generatedKotlinFiles, otherGeneratedFiles, warnings);
267
        }
1✔
268

269
        // now save the files
270
        if (writeFiles) {
1!
271
            callback.saveStarted(generatedXmlFiles.size()
×
272
                    + generatedJavaFiles.size());
×
273

274
            for (GeneratedXmlFile gxf : generatedXmlFiles) {
×
275
                projects.add(gxf.getTargetProject());
×
276
                writeGeneratedXmlFile(gxf, callback);
×
277
            }
×
278

279
            for (GeneratedJavaFile gjf : generatedJavaFiles) {
×
280
                projects.add(gjf.getTargetProject());
×
281
                writeGeneratedJavaFile(gjf, callback);
×
282
            }
×
283

284
            for (GeneratedKotlinFile gkf : generatedKotlinFiles) {
×
285
                projects.add(gkf.getTargetProject());
×
286
                writeGeneratedFile(gkf, callback);
×
287
            }
×
288

289
            for (GeneratedFile gf : otherGeneratedFiles) {
×
290
                projects.add(gf.getTargetProject());
×
291
                writeGeneratedFile(gf, callback);
×
292
            }
×
293

294
            for (String project : projects) {
×
295
                shellCallback.refreshProject(project);
×
296
            }
×
297
        }
298

299
        callback.done();
1✔
300
    }
1✔
301

302
    private void writeGeneratedJavaFile(GeneratedJavaFile gjf, ProgressCallback callback)
303
            throws InterruptedException, IOException {
304
        Path targetFile;
305
        String source;
306
        try {
307
            File directory = shellCallback.getDirectory(gjf
×
308
                    .getTargetProject(), gjf.getTargetPackage());
×
309
            targetFile = directory.toPath().resolve(gjf.getFileName());
×
310
            if (Files.exists(targetFile)) {
×
311
                if (shellCallback.isMergeSupported()) {
×
312
                    source = shellCallback.mergeJavaFile(gjf
×
313
                            .getFormattedContent(), targetFile.toFile(),
×
314
                            MergeConstants.getOldElementTags(),
×
NEW
315
                            gjf.getFileEncoding().orElse(null));
×
316
                } else if (shellCallback.isOverwriteEnabled()) {
×
317
                    source = gjf.getFormattedContent();
×
318
                    warnings.add(getString("Warning.11", //$NON-NLS-1$
×
319
                            targetFile.toFile().getAbsolutePath()));
×
320
                } else {
321
                    source = gjf.getFormattedContent();
×
322
                    targetFile = getUniqueFileName(directory, gjf
×
323
                            .getFileName());
×
324
                    warnings.add(getString(
×
325
                            "Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
×
326
                }
327
            } else {
328
                source = gjf.getFormattedContent();
×
329
            }
330

331
            callback.checkCancel();
×
332
            callback.startTask(getString(
×
333
                    "Progress.15", targetFile.toString())); //$NON-NLS-1$
×
NEW
334
            writeFile(targetFile.toFile(), source, gjf.getFileEncoding().orElse(null));
×
335
        } catch (ShellException e) {
×
336
            warnings.add(e.getMessage());
×
337
        }
×
338
    }
×
339

340
    private void writeGeneratedFile(GeneratedFile gf, ProgressCallback callback)
341
            throws InterruptedException, IOException {
342
        Path targetFile;
343
        String source;
344
        try {
345
            File directory = shellCallback.getDirectory(gf
×
346
                    .getTargetProject(), gf.getTargetPackage());
×
347
            targetFile = directory.toPath().resolve(gf.getFileName());
×
348
            if (Files.exists(targetFile)) {
×
349
                if (shellCallback.isOverwriteEnabled()) {
×
350
                    source = gf.getFormattedContent();
×
351
                    warnings.add(getString("Warning.11", //$NON-NLS-1$
×
352
                            targetFile.toFile().getAbsolutePath()));
×
353
                } else {
354
                    source = gf.getFormattedContent();
×
355
                    targetFile = getUniqueFileName(directory, gf
×
356
                            .getFileName());
×
357
                    warnings.add(getString(
×
358
                            "Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
×
359
                }
360
            } else {
361
                source = gf.getFormattedContent();
×
362
            }
363

364
            callback.checkCancel();
×
365
            callback.startTask(getString(
×
366
                    "Progress.15", targetFile.toString())); //$NON-NLS-1$
×
NEW
367
            writeFile(targetFile.toFile(), source, gf.getFileEncoding().orElse(null));
×
368
        } catch (ShellException e) {
×
369
            warnings.add(e.getMessage());
×
370
        }
×
371
    }
×
372

373
    private void writeGeneratedXmlFile(GeneratedXmlFile gxf, ProgressCallback callback)
374
            throws InterruptedException, IOException {
375
        Path targetFile;
376
        String source;
377
        try {
378
            File directory = shellCallback.getDirectory(gxf
×
379
                    .getTargetProject(), gxf.getTargetPackage());
×
380
            targetFile = directory.toPath().resolve(gxf.getFileName());
×
381
            if (Files.exists(targetFile)) {
×
382
                if (gxf.isMergeable()) {
×
383
                    source = XmlFileMergerJaxp.getMergedSource(gxf,
×
384
                            targetFile.toFile());
×
385
                } else if (shellCallback.isOverwriteEnabled()) {
×
386
                    source = gxf.getFormattedContent();
×
387
                    warnings.add(getString("Warning.11", //$NON-NLS-1$
×
388
                            targetFile.toFile().getAbsolutePath()));
×
389
                } else {
390
                    source = gxf.getFormattedContent();
×
391
                    targetFile = getUniqueFileName(directory, gxf
×
392
                            .getFileName());
×
393
                    warnings.add(getString(
×
394
                            "Warning.2", targetFile.toFile().getAbsolutePath())); //$NON-NLS-1$
×
395
                }
396
            } else {
397
                source = gxf.getFormattedContent();
×
398
            }
399

400
            callback.checkCancel();
×
401
            callback.startTask(getString(
×
402
                    "Progress.15", targetFile.toString())); //$NON-NLS-1$
×
NEW
403
            writeFile(targetFile.toFile(), source, gxf.getFileEncoding().orElse(null));
×
404
        } catch (ShellException e) {
×
405
            warnings.add(e.getMessage());
×
406
        }
×
407
    }
×
408

409
    /**
410
     * Writes, or overwrites, the contents of the specified file.
411
     *
412
     * @param file
413
     *            the file
414
     * @param content
415
     *            the content
416
     * @param fileEncoding
417
     *            the file encoding
418
     * @throws IOException
419
     *             Signals that an I/O exception has occurred.
420
     */
421
    private void writeFile(File file, String content, @Nullable String fileEncoding) throws IOException {
NEW
422
        try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE,
×
423
                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;
1✔
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;
1✔
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;
1✔
503
    }
504
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc