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 }