1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
59
60
61
62
63
64
65
66
67
68
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"));
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
102
103
104
105
106
107
108
109
110
111
112
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
123
124
125
126
127
128
129
130
131
132
133
134
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
243 int totalSteps = generationEngines.stream().mapToInt(GenerationEngine::getGenerationSteps).sum();
244 progressCallback.generationStarted(totalSteps);
245
246
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);
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()));
326 } else {
327 targetFile = getUniqueFileName(directory, gf.getFileName());
328 warnings.add(getString("Warning.2", targetFile.toFile().getAbsolutePath()));
329 }
330 }
331
332 progressCallback.checkCancel();
333 progressCallback.startTask(getString("Progress.15", targetFile.toString()));
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
345
346
347
348
349
350
351
352
353
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
369
370
371
372
373
374
375
376 private Path getUniqueFileName(File directory, String fileName) {
377 Path answer = null;
378
379
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()));
396 }
397
398 return answer;
399 }
400
401
402
403
404
405
406
407
408 public List<GeneratedJavaFile> getGeneratedJavaFiles() {
409 return generationResultsList.stream()
410 .map(GenerationResults::generatedJavaFiles)
411 .flatMap(Collection::stream)
412 .toList();
413 }
414
415
416
417
418
419
420
421
422 public List<GeneratedKotlinFile> getGeneratedKotlinFiles() {
423 return generationResultsList.stream()
424 .map(GenerationResults::generatedKotlinFiles)
425 .flatMap(Collection::stream)
426 .toList();
427 }
428
429
430
431
432
433
434
435
436 public List<GeneratedXmlFile> getGeneratedXmlFiles() {
437 return generationResultsList.stream()
438 .map(GenerationResults::generatedXmlFiles)
439 .flatMap(Collection::stream)
440 .toList();
441 }
442
443
444
445
446
447
448
449
450
451
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
498
499
500
501
502
503
504
505 public Builder withContextIds(Set<String> contextIds) {
506 this.contextIds.addAll(contextIds);
507 return this;
508 }
509
510
511
512
513
514
515
516
517
518
519
520
521 public Builder withFullyQualifiedTableNames(Set<String> fullyQualifiedTableNames) {
522 this.fullyQualifiedTableNames.addAll(fullyQualifiedTableNames);
523 return this;
524 }
525
526
527
528
529
530
531
532
533
534
535 public Builder withOverwriteEnabled(boolean overwriteEnabled) {
536 this.isOverwriteEnabled = overwriteEnabled;
537 return this;
538 }
539
540
541
542
543
544
545
546
547
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 }