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

mybatis / generator / 2001

31 Jan 2026 08:51PM UTC coverage: 89.81% (+0.02%) from 89.791%
2001

Pull #1433

github

web-flow
Merge 1fcd70e5e into 4a679f110
Pull Request #1433: Java Merger Improvements

2269 of 3051 branches covered (74.37%)

37 of 40 new or added lines in 3 files covered. (92.5%)

11555 of 12866 relevant lines covered (89.81%)

0.9 hits per line

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

79.83
/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
import java.util.LinkedHashSet;
26
import java.util.Set;
27

28
import com.github.javaparser.JavaParser;
29
import com.github.javaparser.ParseResult;
30
import com.github.javaparser.ast.CompilationUnit;
31
import com.github.javaparser.ast.ImportDeclaration;
32
import com.github.javaparser.ast.body.BodyDeclaration;
33
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
34
import com.github.javaparser.ast.body.TypeDeclaration;
35
import com.github.javaparser.ast.expr.AnnotationExpr;
36
import com.github.javaparser.ast.expr.Expression;
37
import com.github.javaparser.ast.expr.MemberValuePair;
38
import com.github.javaparser.ast.expr.StringLiteralExpr;
39
import org.jspecify.annotations.Nullable;
40
import org.mybatis.generator.api.MyBatisGenerator;
41
import org.mybatis.generator.config.MergeConstants;
42
import org.mybatis.generator.exception.ShellException;
43

44
/**
45
 * This class handles the task of merging changes into an existing Java file using JavaParser.
46
 * It supports merging by removing methods and fields that have specific JavaDoc tags or annotations.
47
 *
48
 * @author Freeman
49
 */
50
public class JavaFileMerger {
51
    private JavaFileMerger() {
52
    }
53

54
    /**
55
     * Merge a newly generated Java file with an existing Java file.
56
     *
57
     * @param newFileSource the source of the newly generated Java file
58
     * @param existingFile  the existing Java file
59
     * @param javadocTags   the JavaDoc tags that denote which methods and fields in the old file to delete
60
     * @param fileEncoding  the file encoding for reading existing Java files
61
     * @return the merged source, properly formatted
62
     * @throws ShellException if the file cannot be merged for some reason
63
     */
64
    public static String getMergedSource(String newFileSource, File existingFile,
65
                                         String[] javadocTags, @Nullable String fileEncoding) throws ShellException {
66
        try {
67
            String existingFileContent = readFileContent(existingFile, fileEncoding);
×
68
            return getMergedSource(newFileSource, existingFileContent, javadocTags);
×
69
        } catch (IOException e) {
×
70
            throw new ShellException(getString("Warning.13", existingFile.getName()), e);
×
71
        }
72
    }
73

74
    /**
75
     * Merge a newly generated Java file with existing Java file content.
76
     *
77
     * @param newFileSource       the source of the newly generated Java file
78
     * @param existingFileContent the content of the existing Java file
79
     * @param javadocTags         the JavaDoc tags that denote which methods and fields in the old file to delete
80
     * @return the merged source, properly formatted
81
     * @throws ShellException if the file cannot be merged for some reason
82
     */
83
    public static String getMergedSource(String newFileSource, String existingFileContent,
84
                                         String[] javadocTags) throws ShellException {
85
        try {
86
            JavaParser javaParser = new JavaParser();
1✔
87

88
            // Parse the new file
89
            ParseResult<CompilationUnit> newParseResult = javaParser.parse(newFileSource);
1✔
90
            if (!newParseResult.isSuccessful()) {
1!
91
                throw new ShellException("Failed to parse new Java file: " + newParseResult.getProblems());
×
92
            }
93
            CompilationUnit newCompilationUnit = newParseResult.getResult().orElseThrow();
1✔
94

95
            // Parse the existing file
96
            ParseResult<CompilationUnit> existingParseResult = javaParser.parse(existingFileContent);
1✔
97
            if (!existingParseResult.isSuccessful()) {
1!
98
                throw new ShellException("Failed to parse existing Java file: " + existingParseResult.getProblems());
×
99
            }
100
            CompilationUnit existingCompilationUnit = existingParseResult.getResult().orElseThrow();
1✔
101

102
            // Perform the merge
103
            CompilationUnit mergedCompilationUnit =
1✔
104
                    performMerge(newCompilationUnit, existingCompilationUnit, javadocTags);
1✔
105

106
            return mergedCompilationUnit.toString();
1✔
107
        } catch (Exception e) {
×
108
            throw new ShellException("Error merging Java files: " + e.getMessage(), e);
×
109
        }
110
    }
111

112
    private static CompilationUnit performMerge(CompilationUnit newCompilationUnit,
113
                                                CompilationUnit existingCompilationUnit,
114
                                                String[] javadocTags) {
115
        // Start with the new compilation unit as the base (to get new generated elements first)
116
        CompilationUnit mergedCompilationUnit = newCompilationUnit.clone();
1✔
117

118
        // Merge imports
119
        mergeImports(existingCompilationUnit, mergedCompilationUnit);
1✔
120

121
        // Add preserved (non-generated) elements from existing file at the end
122
        addPreservedElements(existingCompilationUnit, mergedCompilationUnit, javadocTags);
1✔
123

124
        return mergedCompilationUnit;
1✔
125
    }
126

127
    private static boolean isGeneratedElement(BodyDeclaration<?> member, String[] javadocTags) {
128
        return hasGeneratedAnnotation(member) || hasGeneratedJavadocTag(member, javadocTags);
1✔
129
    }
130

131
    private static boolean hasGeneratedAnnotation(BodyDeclaration<?> member) {
132
        for (AnnotationExpr annotation : member.getAnnotations()) {
1✔
133
            String annotationName = annotation.getNameAsString();
1✔
134
            // Check for @Generated annotation (both javax and jakarta packages)
135
            if ("Generated".equals(annotationName)//$NON-NLS-1$
1!
NEW
136
                    || "javax.annotation.Generated".equals(annotationName)//$NON-NLS-1$
×
NEW
137
                    || "jakarta.annotation.Generated".equals(annotationName)) { //$NON-NLS-1$
×
138
                return isOurGeneratedAnnotation(annotation);
1✔
139
            }
140
        }
×
141
        return false;
1✔
142
    }
143

144
    private static boolean isOurGeneratedAnnotation(AnnotationExpr annotationExpr) {
145
        if (annotationExpr.isSingleMemberAnnotationExpr()) {
1✔
146
            Expression value = annotationExpr.asSingleMemberAnnotationExpr().getMemberValue();
1✔
147
            if (value.isStringLiteralExpr()) {
1!
148
                return annotationValueMatchesMyBatisGenerator(value.asStringLiteralExpr());
1✔
149
            }
150
        } else if (annotationExpr.isNormalAnnotationExpr()) {
1!
151
            return annotationExpr.asNormalAnnotationExpr().getPairs().stream()
1✔
152
                    .filter(JavaFileMerger::isValuePair)
1✔
153
                    .map(MemberValuePair::getValue)
1✔
154
                    .filter(Expression::isStringLiteralExpr)
1✔
155
                    .map(Expression::asStringLiteralExpr)
1✔
156
                    .findFirst()
1✔
157
                    .map(JavaFileMerger::annotationValueMatchesMyBatisGenerator)
1✔
158
                    .orElse(false);
1✔
159
        }
160

NEW
161
        return false;
×
162
    }
163

164
    private static boolean isValuePair(MemberValuePair pair) {
165
        return pair.getName().asString().equals("value");//$NON-NLS-1$
1✔
166
    }
167

168
    private static boolean annotationValueMatchesMyBatisGenerator(StringLiteralExpr expr) {
169
        return expr.asString().equals(MyBatisGenerator.class.getName());
1✔
170
    }
171

172
    private static boolean hasGeneratedJavadocTag(BodyDeclaration<?> member, String[] javadocTags) {
173
        // Check if the member has a comment and if it contains any of the javadoc tags
174
        if (member.getComment().isPresent()) {
1✔
175
            String commentContent = member.getComment().orElseThrow().getContent();
1✔
176
            for (String tag : javadocTags) {
1✔
177
                if (commentContent.contains(tag)) {
1✔
178
                    return !commentContent.contains(MergeConstants.DO_NOT_DELETE_DURING_MERGE);
1✔
179
                }
180
            }
181
        }
182
        return false;
1✔
183
    }
184

185
    private static void mergeImports(CompilationUnit existingCompilationUnit,
186
                                     CompilationUnit mergedCompilationUnit) {
187
        record ImportInfo(String name, boolean isStatic, boolean isAsterisk) implements Comparable<ImportInfo> {
1✔
188
            @Override
189
            public int compareTo(ImportInfo other) {
190
                // Static imports come last
191
                if (this.isStatic != other.isStatic) {
1!
192
                    return this.isStatic ? 1 : -1;
×
193
                }
194

195
                // Within the same category (static or non-static), sort by import order priority
196
                int priorityThis = getImportPriority(this.name);
1✔
197
                int priorityOther = getImportPriority(other.name);
1✔
198

199
                if (priorityThis != priorityOther) {
1!
200
                    return Integer.compare(priorityThis, priorityOther);
×
201
                }
202

203
                // Within the same priority, use natural ordering (case-insensitive)
204
                return String.CASE_INSENSITIVE_ORDER.compare(this.name, other.name);
1✔
205
            }
206
        }
207

208
        // Collect all imports from both compilation units
209
        Set<ImportInfo> allImports = new LinkedHashSet<>();
1✔
210

211
        // Add imports from new file
212
        for (ImportDeclaration importDecl : mergedCompilationUnit.getImports()) {
1✔
213
            allImports.add(
1✔
214
                    new ImportInfo(importDecl.getNameAsString(), importDecl.isStatic(), importDecl.isAsterisk()));
1✔
215
        }
1✔
216

217
        // Add imports from existing file (avoiding duplicates)
218
        for (ImportDeclaration importDecl : existingCompilationUnit.getImports()) {
1✔
219
            allImports.add(
1✔
220
                    new ImportInfo(importDecl.getNameAsString(), importDecl.isStatic(), importDecl.isAsterisk()));
1✔
221
        }
1✔
222

223
        // Clear existing imports and add sorted imports
224
        mergedCompilationUnit.getImports().clear();
1✔
225

226
        // Sort imports according to best practices and add them back
227
        allImports.stream()
1✔
228
                .sorted()
1✔
229
                .forEach(importInfo -> mergedCompilationUnit.addImport(
1✔
230
                        importInfo.name(), importInfo.isStatic(), importInfo.isAsterisk()));
1✔
231
    }
1✔
232

233
    private static int getImportPriority(String importName) {
234
        if (importName.startsWith("java.")) {
1!
235
            return 10;
1✔
236
        } else if (importName.startsWith("javax.")) {
×
237
            return 20;
×
238
        } else if (importName.startsWith("jakarta.")) {
×
239
            return 30;
×
240
        } else {
241
            return 40; // Third-party and project imports
×
242
        }
243
    }
244

245
    private static void addPreservedElements(CompilationUnit existingCompilationUnit,
246
                                             CompilationUnit mergedCompilationUnit, String[] javadocTags) {
247
        // Find the main type declarations
248
        TypeDeclaration<?> existingTypeDeclaration = findMainTypeDeclaration(existingCompilationUnit);
1✔
249
        TypeDeclaration<?> mergedTypeDeclaration = findMainTypeDeclaration(mergedCompilationUnit);
1✔
250

251
        if (existingTypeDeclaration instanceof ClassOrInterfaceDeclaration existingClassDeclaration
1!
252
                && mergedTypeDeclaration instanceof ClassOrInterfaceDeclaration mergedClassDeclaration) {
1✔
253

254
            // Add only non-generated members from the existing class to the end of merged class
255
            for (BodyDeclaration<?> member : existingClassDeclaration.getMembers()) {
1✔
256
                if (!isGeneratedElement(member, javadocTags)) {
1✔
257
                    // If there is a member in the merged type that matches an existing member, we need to delete it.
258
                    // Some generated elements could survive if they have the do_not_delete_during_merge text
259
                    deleteDuplicateMemberIfExists(mergedClassDeclaration, member);
1✔
260
                    mergedClassDeclaration.addMember(member.clone());
1✔
261
                }
262
            }
1✔
263
        }
264
    }
1✔
265

266
    private static void deleteDuplicateMemberIfExists(TypeDeclaration<?> mergedTypeDeclaration,
267
                                                      BodyDeclaration<?> member) {
268
        mergedTypeDeclaration.getMembers().stream()
1✔
269
                .filter(td -> membersMatch(td, member))
1✔
270
                .findFirst()
1✔
271
                .ifPresent(mergedTypeDeclaration::remove);
1✔
272
    }
1✔
273

274
    private static boolean membersMatch(BodyDeclaration<?> member1, BodyDeclaration<?> member2) {
275
        if (member1.isTypeDeclaration() && member2.isTypeDeclaration()) {
1✔
276
            return member1.asTypeDeclaration().getNameAsString()
1✔
277
                    .equals(member2.asTypeDeclaration().getNameAsString());
1✔
278
        } else if (member1.isCallableDeclaration() && member2.isCallableDeclaration()) {
1✔
279
            return member1.asCallableDeclaration().getSignature().asString()
1✔
280
                    .equals(member2.asCallableDeclaration().getSignature().asString());
1✔
281
        } else if (member1.isFieldDeclaration() && member2.isFieldDeclaration()) {
1✔
282
            return member1.asFieldDeclaration().toString()
1✔
283
                    .equals(member2.asFieldDeclaration().toString());
1✔
284
        }
285

286
        return false;
1✔
287
    }
288

289
    private static @Nullable TypeDeclaration<?> findMainTypeDeclaration(CompilationUnit compilationUnit) {
290
        // Return the first public type declaration, or the first type declaration if no public one exists
291
        TypeDeclaration<?> firstType = null;
1✔
292
        for (TypeDeclaration<?> typeDeclaration : compilationUnit.getTypes()) {
1!
293
            if (firstType == null) {
1!
294
                firstType = typeDeclaration;
1✔
295
            }
296
            if (typeDeclaration.isPublic()) {
1!
297
                return typeDeclaration;
1✔
298
            }
299
        }
×
300
        return firstType;
×
301
    }
302

303
    private static String readFileContent(File file, @Nullable String fileEncoding) throws IOException {
304
        if (fileEncoding != null) {
×
305
            return Files.readString(file.toPath(), Charset.forName(fileEncoding));
×
306
        } else {
307
            return Files.readString(file.toPath(), StandardCharsets.UTF_8);
×
308
        }
309
    }
310
}
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