XMLCleanupService.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * 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.corext.fix.helper;

import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

/** Standalone service for conflict-safe XML workspace cleanup. */
public class XMLCleanupService {

	private static final ILog LOG= Platform.getLog(XMLCleanupService.class);
	private static final String PLUGIN_ID= "sandbox_xml_cleanup"; //$NON-NLS-1$
	private static final Set<String> PDE_FILE_NAMES= Set.of(
			"plugin.xml", "feature.xml", "fragment.xml"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	private static final Set<String> PDE_EXTENSIONS= Set.of("exsd", "xsd"); //$NON-NLS-1$ //$NON-NLS-2$
	private static final Set<String> PDE_DIRECTORIES= Set.of("OSGI-INF", "META-INF"); //$NON-NLS-1$ //$NON-NLS-2$

	private boolean enableIndent;

	/** Sets whether transformed markup is indented. */
	public void setEnableIndent(boolean enable) {
		enableIndent= enable;
	}

	/**
	 * Processes one relevant XML resource.
	 *
	 * @return {@code true} when bytes were changed
	 * @throws CoreException when the resource is dirty, concurrently modified,
	 *             too large, ambiguously encoded, malformed, or cannot be written
	 */
	public boolean processFile(IFile file, IProgressMonitor monitor) throws CoreException {
		if (monitor != null && monitor.isCanceled() || !isPDERelevantFile(file)) {
			return false;
		}
		XMLResourceSupport.Transformation transformation=
				XMLResourceSupport.prepare(file, enableIndent, monitor);
		if (!transformation.changed()) {
			return false;
		}
		int originalBytes= transformation.snapshot().originalBytes().length;
		int newBytes= transformation.transformedBytes().length;
		XMLResourceSupport.write(file, transformation, monitor);
		int bytesSaved= originalBytes - newBytes;
		double percentSaved= originalBytes > 0 ? bytesSaved * 100.0 / originalBytes : 0.0;
		LOG.log(new Status(IStatus.INFO, PLUGIN_ID,
				"Applied transformation to: " + file.getFullPath() //$NON-NLS-1$
						+ String.format(" | Size: %d → %d bytes, saved: %d bytes (%.1f%%)", //$NON-NLS-1$
								originalBytes, newBytes, bytesSaved, percentSaved)));
		return true;
	}

	/** Processes all relevant XML files in a project. */
	public int processProject(IProject project, IProgressMonitor monitor) throws CoreException {
		int[] filesProcessed= { 0 };
		project.accept(resource -> {
			if (monitor != null && monitor.isCanceled()) {
				return false;
			}
			if (resource instanceof IFile file && isPDERelevantFile(file)) {
				try {
					if (monitor != null) {
						monitor.subTask("Processing: " + file.getFullPath()); //$NON-NLS-1$
					}
					if (processFile(file, monitor)) {
						filesProcessed[0]++;
					}
				} catch (CoreException e) {
					LOG.log(e.getStatus());
				}
				if (monitor != null) {
					monitor.worked(1);
				}
			}
			return true;
		});
		return filesProcessed[0];
	}

	/** Returns whether the resource belongs to the supported PDE XML scope. */
	public boolean isPDERelevantFile(IFile file) {
		String extension= file.getFileExtension();
		return (PDE_FILE_NAMES.contains(file.getName())
				|| extension != null && PDE_EXTENSIONS.contains(extension))
				&& isInPDELocation(file);
	}

	private static boolean isInPDELocation(IFile file) {
		IResource current= file.getParent();
		if (current instanceof IProject) {
			return true;
		}
		while (current != null && !(current instanceof IProject)) {
			if (current instanceof IFolder folder) {
				String name= folder.getName();
				if (PDE_DIRECTORIES.contains(name) || "schema".equalsIgnoreCase(name)) { //$NON-NLS-1$
					return true;
				}
			}
			current= current.getParent();
		}
		return false;
	}
}