JavaMergeUtilities.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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName;
import com.github.javaparser.ast.type.ClassOrInterfaceType;

public class JavaMergeUtilities {
    private JavaMergeUtilities() {
        // utility class, no instances
    }

    /**
     * Compare two compilation units and find imports that are in the existing file but not in the new file.
     * We assume this means they are required for some custom method, so we will add them to the new
     * file if there are other items to merge. This may create unused imports in the new file if the
     * initial assumption is incorrect, but better safe than sorry.
     *
     * @param existingCompilationUnit compilation unit representing the existing file
     * @param newCompilationUnit compilation unit representing the new file
     */
    public static List<ImportDeclaration> findCustomImports(CompilationUnit existingCompilationUnit,
                                                            CompilationUnit newCompilationUnit) {
        List<ImportDeclaration> customImports = new ArrayList<>();

        List<String> newFileImports = newCompilationUnit.getImports().stream()
                .map(JavaMergeUtilities::stringify).toList();

        for (ImportDeclaration id : existingCompilationUnit.getImports()) {
            if (!newFileImports.contains(stringify(id))) {
                customImports.add(id);
            }
        }

        return customImports;
    }

    /**
     * Compare two members to see if they are "functionally equivalent". This is defined as:
     *
     * <ul>
     *     <li>Members are the same type</li>
     *     <li>Members have the same signature or basic declaration</li>
     * </ul>
     *
     * @param member1 the first member
     * @param member2 the second member
     * @return true if the members are functionally equivalent
     */
    public static boolean membersMatch(BodyDeclaration<?> member1, BodyDeclaration<?> member2) {
        if (member1.isTypeDeclaration() && member2.isTypeDeclaration()) {
            return member1.asTypeDeclaration().getNameAsString()
                    .equals(member2.asTypeDeclaration().getNameAsString());
        } else if (member1.isCallableDeclaration() && member2.isCallableDeclaration()) {
            return member1.asCallableDeclaration().getSignature().asString()
                    .equals(member2.asCallableDeclaration().getSignature().asString());
        } else if (member1.isFieldDeclaration() && member2.isFieldDeclaration()) {
            return stringify(member1.asFieldDeclaration()).equals(stringify(member2.asFieldDeclaration()));
        }

        return false;
    }

    public static List<ClassOrInterfaceType> findCustomSuperInterfaces(BodyDeclaration<?> existingType,
                                                                       BodyDeclaration<?> newType) {
        List<ClassOrInterfaceType> customSuperInterfaces = new ArrayList<>();

        List<String> newFileSuperInterfaces = findSuperInterfaces(newType).stream()
                .map(NodeWithSimpleName::getNameAsString).toList();

        for (ClassOrInterfaceType id : findSuperInterfaces(existingType)) {
            if (!newFileSuperInterfaces.contains(id.getNameAsString())) {
                customSuperInterfaces.add(id);
            }
        }

        return customSuperInterfaces;
    }

    public static List<ClassOrInterfaceType> findSuperInterfaces(BodyDeclaration<?> bodyDeclaration) {
        if (bodyDeclaration.isClassOrInterfaceDeclaration()) {
            return bodyDeclaration.asClassOrInterfaceDeclaration().getImplementedTypes();
        } else if (bodyDeclaration.isEnumDeclaration()) {
            return bodyDeclaration.asEnumDeclaration().getImplementedTypes();
        } else if (bodyDeclaration.isRecordDeclaration()) {
            return bodyDeclaration.asRecordDeclaration().getImplementedTypes();
        }

        return Collections.emptyList();
    }

    public static void addSuperInterface(BodyDeclaration<?> bodyDeclaration, ClassOrInterfaceType superInterface) {
        if (bodyDeclaration.isClassOrInterfaceDeclaration()) {
            bodyDeclaration.asClassOrInterfaceDeclaration().addImplementedType(superInterface);
        } else if (bodyDeclaration.isEnumDeclaration()) {
            bodyDeclaration.asEnumDeclaration().addImplementedType(superInterface);
        } else if (bodyDeclaration.isRecordDeclaration()) {
            bodyDeclaration.asRecordDeclaration().addImplementedType(superInterface);
        }
    }

    /**
     * Create a string representation of an import that we can use to find matches.
     *
     * @param importDeclaration the import declaration to stringify
     * @return string representation of the import (not a full import statement)
     */
    public static String stringify(ImportDeclaration importDeclaration) {
        StringBuilder sb = new StringBuilder();
        if (importDeclaration.isStatic()) {
            sb.append("static "); //$NON-NLS-1$
        }
        if (importDeclaration.isModule()) {
            sb.append("module "); //$NON-NLS-1$
        }
        sb.append(importDeclaration.getNameAsString());
        if (importDeclaration.isAsterisk()) {
            sb.append(".*"); //$NON-NLS-1$
        }

        return sb.toString();
    }

    public static String stringify(FieldDeclaration fieldDeclaration) {
        return fieldDeclaration.getVariables().stream()
                .map(JavaMergeUtilities::stringify)
                .collect(Collectors.joining(",")); //$NON-NLS-1$
    }

    public static String stringify(VariableDeclarator variableDeclarator) {
        return variableDeclarator.getType().toString()
                + " " //$NON-NLS-1$
                + variableDeclarator.getName().toString();
    }
}