CharsetCleanUpCoordinator.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * This program and the accompanying materials are made available under the terms
 * of the Eclipse Public License 2.0 which accompanies this distribution, and is
 * available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.internal.ui.fix;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.ui.cleanup.CleanUpContext;
import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.sandbox.jdt.cleanup.multifile.AbstractPlannedMultiFileCleanUp;
import org.sandbox.jdt.cleanup.multifile.MultiFileCandidateDiagnostic;
import org.sandbox.jdt.cleanup.multifile.MultiFileCleanUpDiagnostics;
import org.sandbox.jdt.cleanup.multifile.MultiFileCleanUpPlanResult;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningMetrics;
import org.sandbox.jdt.cleanup.multifile.MultiFileScopeDiagnostic;
import org.sandbox.jdt.triggerpattern.cleanup.CharsetConstructorMigration;
import org.sandbox.jdt.triggerpattern.cleanup.CheckedExceptionMigration;

/** Reuses the common lifecycle; only explicitly selected/discovered files can be changed. */
final class CharsetCleanUpCoordinator extends AbstractPlannedMultiFileCleanUp<CharsetCleanUpCoordinator.Run> {
    record Run(CheckedExceptionMigration.Plan exceptions) { }
    private final UseExplicitEncodingCleanUpCore owner;

    CharsetCleanUpCoordinator(UseExplicitEncodingCleanUpCore owner) {
        this.owner= owner;
    }

    boolean isPrepared(IJavaProject project) { return getPlan(project) != null; }

    @Override protected MultiFileCleanUpPlanResult<Run> createPlan(IJavaProject project,
            ICompilationUnit[] units, IProgressMonitor monitor) throws CoreException {
        CheckedExceptionMigration.Plan plan= owner.requireAST() ? parseAndPlan(project, List.of(units), monitor) : null;
        RefactoringStatus status= new RefactoringStatus();
        if (plan == null) return MultiFileCleanUpPlanResult.success(new Run(null));
        try {
            plan.requireComplete();
        } catch (CoreException conflict) {
            status.addFatalError(conflict.getStatus().getMessage());
            return new MultiFileCleanUpPlanResult<>(null, status);
        }
        List<String> affected= plan.affectedCompilationUnits().stream().sorted().toList();
        var scope= new MultiFileScopeDiagnostic(List.of(units).stream().map(ICompilationUnit::getHandleIdentifier).toList(),
                List.of(), "EXPLICIT_SELECTED_SCOPE", "Charset calls and their source exception contracts are changed together.", true); //$NON-NLS-1$ //$NON-NLS-2$
        var candidate= MultiFileCandidateDiagnostic.transformed("charset-exceptions", affected.get(0), //$NON-NLS-1$
                "Use typed Charset constructors and adapt checked exceptions", affected.subList(1, affected.size())); //$NON-NLS-1$
        return MultiFileCleanUpPlanResult.success(new Run(plan), status, MultiFilePlanningMetrics.empty(),
                new MultiFileCleanUpDiagnostics("sandbox.encoding", scope, List.of(candidate))); //$NON-NLS-1$
    }

    @Override protected ICleanUpFix createFixForPlan(Run plan, CleanUpContext context) throws CoreException {
        return owner.createEncodingFix(context, plan.exceptions());
    }

    @Override protected Collection<ICompilationUnit> discoverAdditionalCompilationUnits(IJavaProject project,
            Collection<ICompilationUnit> selected, IProgressMonitor monitor) throws CoreException {
        if (!owner.requireAST()) return List.of();
        var plan= parseAndPlan(project, selected, monitor);
        if (plan == null) return List.of();
        List<ICompilationUnit> additions= new ArrayList<>();
        for (String handle : plan.requiredCompilationUnits()) {
            if (JavaCore.create(handle) instanceof ICompilationUnit unit && unit.exists()
                    && project.equals(unit.getJavaProject()) && !selected.contains(unit)) additions.add(unit);
        }
        additions.sort(Comparator.comparing(ICompilationUnit::getHandleIdentifier));
        return additions;
    }

    private static CheckedExceptionMigration.Plan parseAndPlan(IJavaProject project,
            Collection<ICompilationUnit> units, IProgressMonitor monitor) throws CoreException {
        if (units.isEmpty()) return null;
        List<CompilationUnit> roots= new ArrayList<>();
        ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
        parser.setProject(project);
        parser.setResolveBindings(true);
        parser.createASTs(units.toArray(ICompilationUnit[]::new), new String[0], new ASTRequestor() {
            @Override public void acceptAST(ICompilationUnit unit, CompilationUnit root) { roots.add(root); }
        }, monitor);
        if (monitor != null && monitor.isCanceled()) throw new OperationCanceledException();
        if (roots.size() != units.size()) {
            throw new CoreException(Status.error("Not all selected compilation units could be parsed")); //$NON-NLS-1$
        }
        return CharsetConstructorMigration.plan(roots, monitor);
    }
}