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

mybatis / generator / 2010

03 Feb 2026 08:32PM UTC coverage: 89.961% (+0.2%) from 89.808%
2010

push

github

web-flow
Merge pull request #1437 from jeffgbutler/refactor-javamerger

Java Merger Enhancements - Support Records and Enums

2294 of 3071 branches covered (74.7%)

161 of 170 new or added lines in 5 files covered. (94.71%)

1 existing line in 1 file now uncovered.

11631 of 12929 relevant lines covered (89.96%)

0.9 hits per line

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

85.48
/core/mybatis-generator-core/src/main/java/org/mybatis/generator/merge/java/JavaFileMerger.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.merge.java;
17

18
import static org.mybatis.generator.internal.util.messages.Messages.getString;
19

20
import java.io.File;
21
import java.io.IOException;
22
import java.nio.charset.Charset;
23
import java.nio.charset.StandardCharsets;
24
import java.nio.file.Files;
25

26
import com.github.javaparser.JavaParser;
27
import com.github.javaparser.ParseResult;
28
import com.github.javaparser.ParserConfiguration;
29
import com.github.javaparser.Problem;
30
import com.github.javaparser.ast.CompilationUnit;
31
import com.github.javaparser.ast.body.BodyDeclaration;
32
import com.github.javaparser.ast.body.TypeDeclaration;
33
import com.github.javaparser.printer.DefaultPrettyPrinter;
34
import com.github.javaparser.printer.configuration.PrinterConfiguration;
35
import org.jspecify.annotations.Nullable;
36
import org.mybatis.generator.exception.MultiMessageException;
37
import org.mybatis.generator.exception.ShellException;
38

39
/**
40
 * This class handles the task of merging changes into an existing Java file using JavaParser.
41
 * It supports merging by removing methods and fields that have specific Javadoc tags or annotations.
42
 *
43
 * <p>Given an existing source file and a newly generated file of the same name, the merger will:
44
 * <ol>
45
 *     <li>Parse the existing file looking for custom additions. A custom addition is defined in these ways:
46
 *         <ul>
47
 *             <li>A body element (field, method, nested class, etc.) not marked as generated - missing both a
48
 *                 <code>@Generated</code> annotation and an older style custom Javadoc tag.</li>
49
 *             <li>A body element (field, method, nested class, etc.) marked as generated by an older style custom
50
 *                 Javadoc tag and also containing the phrase "do_not_delete_during_merge".</li>
51
 *             <li>Any import in the existing file that is not present in the newly generated file</li>
52
 *             <li>Any super interface in the existing file that is not present in the newly generated file</li>
53
 *             <li>Any enum constant missing a "generated" marker in the existing file that is not present in the new
54
 *                 file</li>
55
 *         </ul>
56
 *         It is important to know that the parser will only look for direct children of either the public type or the
57
 *         first non-public type in a source file.
58
 *     </li>
59
 *     <li>If there are no custom additions, the newly generated file is returned unmodified.</li>
60
 *     <li>If there are custom additions, then:
61
 *         <ul>
62
 *             <li>Add any imports present in the existing file but missing in the new file</li>
63
 *             <li>Remove any members in the new file that match custom additions in the existing file</li>
64
 *             <li>Add all custom additions from the existing file</li>
65
 *             <li>The merged file is formatted and returned.</li>
66
 *         </ul>
67
 *     </li>
68
 * </ol>
69
 *
70
 * <p>This implementation differs from the original Eclipse-based implementation in the following ways:</p>
71
 * <ol>
72
 *     <li>This implementation supports merging enums and records</li>
73
 *     <li>This implementation supports merging when the existing file is a class or interface, and the newly generated
74
 *         file is a record.
75
 *     </li>
76
 *     <li>This implementation does not support merging the super class from the existing file to the newly generated
77
 *         file. This was always a little dangerous.</li>
78
 * </ol>
79
 *
80
 * @author Freeman (original)
81
 * @author Jeff Butler (refactoring and enhancements)
82
 */
83
public class JavaFileMerger {
84
    private final PrinterConfiguration printerConfiguration;
85

86
    public JavaFileMerger(PrinterConfiguration printerConfiguration) {
1✔
87
        this.printerConfiguration = printerConfiguration;
1✔
88
    }
1✔
89

90
    /**
91
     * Merge a newly generated Java file with an existing Java file.
92
     *
93
     * @param newFileContent the content of the newly generated Java file
94
     * @param existingFile the existing Java file
95
     * @param fileEncoding the file encoding for reading existing Java files
96
     * @return the merged source, properly formatted
97
     * @throws ShellException if the file cannot be merged for some reason
98
     */
99
    public String getMergedSource(String newFileContent, File existingFile,
100
                                         @Nullable String fileEncoding) throws ShellException {
101
        try {
102
            String existingFileContent = readFileContent(existingFile, fileEncoding);
×
NEW
103
            return getMergedSource(newFileContent, existingFileContent);
×
104
        } catch (IOException e) {
×
NEW
105
            throw new ShellException(getString("Warning.32", existingFile.getName()), e); //$NON-NLS-1$
×
106
        }
107
    }
108

109
    /**
110
     * Merge a newly generated Java file with existing Java file content.
111
     *
112
     * @param newFileContent the content of the newly generated Java file
113
     * @param existingFileContent the content of the existing Java file
114
     * @return the merged source, properly formatted
115
     * @throws ShellException if the file cannot be merged for some reason
116
     */
117
    public String getMergedSource(String newFileContent, String existingFileContent) throws ShellException {
118
        ParserConfiguration parserConfiguration = new ParserConfiguration();
1✔
119
        parserConfiguration.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25);
1✔
120
        JavaParser javaParser = new JavaParser(parserConfiguration);
1✔
121

122
        ParseResults existingFileParseResults = parseAndFindMainTypeDeclaration(javaParser, existingFileContent,
1✔
123
                FileType.EXISTING_FILE);
124

125
        // Gather custom members from the existing file. If none, just return the new file as is
126
        CustomMemberGatherer customMemberGatherer = new CustomMemberGatherer(existingFileParseResults.typeDeclaration);
1✔
127
        if (!customMemberGatherer.hasAnyMembersToMerge()) {
1✔
128
            return newFileContent;
1✔
129
        }
130

131
        // Custom members exist, need to merge...
132
        ParseResults newFileParseResults = parseAndFindMainTypeDeclaration(javaParser, newFileContent,
1✔
133
                FileType.NEW_FILE);
134

135
        // Delete elements in the new file that match doNotDeleteMembers from the existing file
136
        customMemberGatherer.doNotDeleteBodyMembers()
1✔
137
                .forEach(m -> deleteDuplicateMemberIfExists(newFileParseResults.typeDeclaration, m));
1✔
138

139
        // Look for custom imports in the existing file and merge into the new file
140
        JavaMergeUtilities
1✔
141
                .findCustomImports(existingFileParseResults.compilationUnit, newFileParseResults.compilationUnit)
1✔
142
                .forEach(newFileParseResults.compilationUnit::addImport);
1✔
143

144
        // Add custom body members from the existing file to the new file
145
        customMemberGatherer.allCustomBodyMembers().forEach(newFileParseResults.typeDeclaration::addMember);
1✔
146

147
        // Add custom enum constants from the existing file to the new file
148
        if (newFileParseResults.typeDeclaration.isEnumDeclaration()) {
1✔
149
            customMemberGatherer.customEnumConstants()
1✔
150
                    .forEach(newFileParseResults.typeDeclaration.asEnumDeclaration()::addEntry);
1✔
151
        }
152

153
        // Look for custom super interfaces in the existing file and merge into the new file
154
        JavaMergeUtilities
1✔
155
                .findCustomSuperInterfaces(existingFileParseResults.typeDeclaration,
1✔
156
                        newFileParseResults.typeDeclaration)
157
                .forEach(t -> JavaMergeUtilities.addSuperInterface(newFileParseResults.typeDeclaration, t));
1✔
158

159
        // Return the new (merged) file
160
        DefaultPrettyPrinter printer = new DefaultPrettyPrinter(printerConfiguration);
1✔
161
        return printer.print(newFileParseResults.compilationUnit);
1✔
162
    }
163

164
    private ParseResults parseAndFindMainTypeDeclaration(JavaParser javaParser, String source, FileType fileType)
165
            throws ShellException {
166
        ParseResult<CompilationUnit> parseResult = javaParser.parse(source);
1✔
167

168
        // little hack to pull the result out of the lambda. This allows us to avoid "orElseThrow()" later on
169
        @Nullable CompilationUnit[] compilationUnits = new CompilationUnit [1];
1✔
170
        parseResult.ifSuccessful(cu -> compilationUnits[0] = cu);
1✔
171

172
        if (compilationUnits[0] == null) {
1✔
173
            var mme = new MultiMessageException(parseResult.getProblems().stream().map(Problem::toString).toList());
1✔
174
            throw new ShellException(getString("RuntimeError.28", fileType.toString()), mme); //$NON-NLS-1$
1✔
175
        }
176

177
        return new ParseResults(compilationUnits[0], findMainTypeDeclaration(compilationUnits[0], fileType));
1✔
178
    }
179

180
    private void deleteDuplicateMemberIfExists(TypeDeclaration<?> newTypeDeclaration,
181
                                                      BodyDeclaration<?> member) {
182
        newTypeDeclaration.getMembers().stream()
1✔
183
                .filter(td -> JavaMergeUtilities.membersMatch(td, member))
1✔
184
                .findFirst()
1✔
185
                .ifPresent(newTypeDeclaration::remove);
1✔
186
    }
1✔
187

188
    private TypeDeclaration<?> findMainTypeDeclaration(CompilationUnit compilationUnit, FileType fileType)
189
            throws ShellException {
190
        // Return the first public type declaration, or the first type declaration if no public one exists
191
        TypeDeclaration<?> firstType = null;
1✔
192
        for (TypeDeclaration<?> typeDeclaration : compilationUnit.getTypes()) {
1✔
193
            if (firstType == null) {
1!
194
                firstType = typeDeclaration;
1✔
195
            }
196
            if (typeDeclaration.isPublic()) {
1!
197
                return typeDeclaration;
1✔
198
            }
199
        }
×
200
        if (firstType == null) {
1!
201
            throw new ShellException(getString("RuntimeError.29", fileType.toString())); //$NON-NLS-1$
1✔
202
        }
UNCOV
203
        return firstType;
×
204
    }
205

206
    private String readFileContent(File file, @Nullable String fileEncoding) throws IOException {
207
        if (fileEncoding != null) {
×
208
            return Files.readString(file.toPath(), Charset.forName(fileEncoding));
×
209
        } else {
210
            return Files.readString(file.toPath(), StandardCharsets.UTF_8);
×
211
        }
212
    }
213

214
    private record ParseResults(CompilationUnit compilationUnit, TypeDeclaration<?> typeDeclaration) {}
1✔
215

216
    private enum FileType {
1✔
217
        NEW_FILE("new Java file"), //$NON-NLS-1$
1✔
218
        EXISTING_FILE("existing Java file"); //$NON-NLS-1$
1✔
219

220
        private final String displayText;
221

222
        FileType(String displayText) {
1✔
223
            this.displayText = displayText;
1✔
224
        }
1✔
225

226
        @Override
227
        public String toString() {
228
            return displayText;
1✔
229
        }
230
    }
231
}
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