JavaFileMergerJavaParserImpl.java
/*
* Copyright 2006-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.generator.merge.java;
import static org.mybatis.generator.internal.util.messages.Messages.getString;
import java.util.List;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.Problem;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.printer.DefaultPrettyPrinter;
import com.github.javaparser.printer.Printer;
import com.github.javaparser.printer.lexicalpreservation.DefaultLexicalPreservingPrinter;
import org.jspecify.annotations.Nullable;
import org.mybatis.generator.exception.MergeException;
/**
* This class handles the task of merging changes into an existing Java file using JavaParser.
* It supports merging by removing methods and fields that have specific Javadoc tags or annotations.
*
* <p>Given an existing source file and a newly generated file of the same name, the merger will:
* <ol>
* <li>Parse the existing file looking for custom additions. A custom addition is defined in these ways:
* <ul>
* <li>A body element (field, method, nested class, etc.) not marked as generated - missing both a
* <code>@Generated</code> annotation and an older style custom Javadoc tag.</li>
* <li>A body element (field, method, nested class, etc.) marked as generated by an older style custom
* Javadoc tag and also containing the phrase "do_not_delete_during_merge".</li>
* <li>Any import in the existing file that is not present in the newly generated file</li>
* <li>Any super interface in the existing file that is not present in the newly generated file</li>
* <li>Any enum constant missing a "generated" marker in the existing file that is not present in the new
* file</li>
* </ul>
* It is important to know that the parser will only look for direct children of either the public type or the
* first non-public type in a source file.
* </li>
* <li>If there are no custom additions, the newly generated file is returned unmodified.</li>
* <li>If there are custom additions, then:
* <ul>
* <li>Add any imports present in the existing file but missing in the new file</li>
* <li>Remove any members in the new file that match custom additions in the existing file</li>
* <li>Add all custom additions from the existing file</li>
* <li>The merged file is formatted and returned.</li>
* </ul>
* </li>
* </ol>
*
* <p>This implementation differs from the original Eclipse-based implementation in the following ways:</p>
* <ol>
* <li>This implementation supports merging enums and records</li>
* <li>This implementation supports merging when the existing file is a class or interface, and the newly generated
* file is a record (the result will be a record).
* </li>
* <li>This implementation does not support merging the super class from an existing file to the newly generated
* file.</li>
* <li>This implementation does not attempt to preserve custom annotations added to generated elements. With the
* generator now generating code with many annotations, it is challenging to distinguish between annotations
* created by MBG, and custom annotations added after code generation by a user. If you need to add
* annotations to generated elements, consider implementing a plugin that will create the annotations whenever
* the generator runs.
* </li>
* </ol>
*
* @author Freeman (original)
* @author Jeff Butler (refactoring and enhancements)
*/
public class JavaFileMergerJavaParserImpl implements JavaFileMerger {
private final Printer printer;
public JavaFileMergerJavaParserImpl(JavaMergerFactory.PrinterConfiguration printerConfiguration) {
printer = switch (printerConfiguration) {
case ECLIPSE -> new DefaultPrettyPrinter(new EclipseOrderedPrinterConfiguration());
case LEXICAL_PRESERVING -> new DefaultLexicalPreservingPrinter();
};
}
/**
* Merge a newly generated Java file with existing Java file content.
*
* @param newFileContent the content of the newly generated Java file
* @param existingFileContent the content of the existing Java file
* @return the merged source, properly formatted
* @throws MergeException if the file cannot be merged for some reason
*/
@Override
public String getMergedSource(String newFileContent, String existingFileContent) throws MergeException {
ParserConfiguration parserConfiguration = new ParserConfiguration();
parserConfiguration.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25);
parserConfiguration.setLexicalPreservationEnabled(true);
JavaParser javaParser = new JavaParser(parserConfiguration);
ParseResults existingFileParseResults = parseAndFindMainTypeDeclaration(javaParser, existingFileContent,
FileType.EXISTING_FILE);
// Gather custom members from the existing file. If none, just return the new file as is
CustomMemberGatherer customMemberGatherer = new CustomMemberGatherer(existingFileParseResults.typeDeclaration);
if (!customMemberGatherer.hasAnyMembersToMerge()) {
return newFileContent;
}
// Custom members exist, need to merge...
ParseResults newFileParseResults = parseAndFindMainTypeDeclaration(javaParser, newFileContent,
FileType.NEW_FILE);
// Delete elements in the new file that match doNotDeleteMembers from the existing file
customMemberGatherer.doNotDeleteBodyMembers()
.forEach(m -> deleteDuplicateMemberIfExists(newFileParseResults.typeDeclaration, m));
// Look for custom imports in the existing file and merge into the new file
JavaMergeUtilities
.findCustomImports(existingFileParseResults.compilationUnit, newFileParseResults.compilationUnit)
.forEach(newFileParseResults.compilationUnit::addImport);
// Add custom body members from the existing file to the new file
customMemberGatherer.allCustomBodyMembers().forEach(newFileParseResults.typeDeclaration::addMember);
// Add custom enum constants from the existing file to the new file
if (newFileParseResults.typeDeclaration.isEnumDeclaration()) {
customMemberGatherer.customEnumConstants()
.forEach(newFileParseResults.typeDeclaration.asEnumDeclaration()::addEntry);
}
// Look for custom super interfaces in the existing file and merge into the new file
JavaMergeUtilities
.findCustomSuperInterfaces(existingFileParseResults.typeDeclaration,
newFileParseResults.typeDeclaration)
.forEach(t -> JavaMergeUtilities.addSuperInterface(newFileParseResults.typeDeclaration, t));
// Return the new (merged) file
return printer.print(newFileParseResults.compilationUnit);
}
private ParseResults parseAndFindMainTypeDeclaration(JavaParser javaParser, String source, FileType fileType)
throws MergeException {
ParseResult<CompilationUnit> parseResult = javaParser.parse(source);
// little hack to pull the result out of the lambda. This allows us to avoid "orElseThrow()" later on
@Nullable CompilationUnit[] compilationUnits = new CompilationUnit [1];
parseResult.ifSuccessful(cu -> compilationUnits[0] = cu);
if (compilationUnits[0] == null) {
List<String> details = parseResult.getProblems().stream()
.map(Problem::toString)
.toList();
throw new MergeException(getString("RuntimeError.28", fileType.toString()), details); //$NON-NLS-1$
}
return new ParseResults(compilationUnits[0], findMainTypeDeclaration(compilationUnits[0], fileType));
}
private void deleteDuplicateMemberIfExists(TypeDeclaration<?> newTypeDeclaration,
BodyDeclaration<?> member) {
newTypeDeclaration.getMembers().stream()
.filter(td -> JavaMergeUtilities.membersMatch(td, member))
.findFirst()
.ifPresent(newTypeDeclaration::remove);
}
private TypeDeclaration<?> findMainTypeDeclaration(CompilationUnit compilationUnit, FileType fileType)
throws MergeException {
// Return the first public type declaration, or the first type declaration if no public one exists
TypeDeclaration<?> firstType = null;
for (TypeDeclaration<?> typeDeclaration : compilationUnit.getTypes()) {
if (firstType == null) {
firstType = typeDeclaration;
}
if (typeDeclaration.isPublic()) {
return typeDeclaration;
}
}
if (firstType == null) {
throw new MergeException(getString("RuntimeError.29", fileType.toString())); //$NON-NLS-1$
}
return firstType;
}
private record ParseResults(CompilationUnit compilationUnit, TypeDeclaration<?> typeDeclaration) {}
private enum FileType {
NEW_FILE("new Java file"), //$NON-NLS-1$
EXISTING_FILE("existing Java file"); //$NON-NLS-1$
private final String displayText;
FileType(String displayText) {
this.displayText = displayText;
}
@Override
public String toString() {
return displayText;
}
}
}