XMLPlugin.java
/*******************************************************************************
* Copyright (c) 2021 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
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.HashSet;
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.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.corext.fix.XMLCleanUpFixCore;
/**
* XML cleanup processor for PDE-relevant files (plugin.xml, feature.xml, fragment.xml, *.exsd, *.xsd).
*/
public class XMLPlugin extends AbstractTool<XMLCandidateHit> {
private static final ILog LOG = Platform.getLog(XMLPlugin.class);
private static final String PLUGIN_ID = "org.sandbox.jdt.internal.corext.fix.helper";
// PDE-relevant file names
private static final Set<String> PDE_FILE_NAMES = Set.of(
"plugin.xml",
"feature.xml",
"fragment.xml"
);
// PDE-relevant file extensions
private static final Set<String> PDE_EXTENSIONS = Set.of("exsd", "xsd");
// PDE-typical directories
private static final Set<String> PDE_DIRECTORIES = Set.of("OSGI-INF", "META-INF");
// Cache for processed files to avoid duplicate processing
private final Set<Path> processedFiles = new HashSet<>();
// Indentation preference (default: false for size reduction)
private boolean enableIndent = false;
/**
* Set whether to enable indentation in XML output.
*
* @param enable true to enable indentation, false for compact output (default)
*/
public void setEnableIndent(boolean enable) {
this.enableIndent = enable;
}
@Override
public void find(XMLCleanUpFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesProcessed,
boolean createForOnlyIfVarUsed) {
// Clear cache at start of each cleanup run to avoid stale file references
processedFiles.clear();
try {
// Get the resource associated with the compilation unit
IResource resource = compilationUnit.getJavaElement().getResource();
if (resource == null || !resource.exists() || !(resource instanceof IFile)) {
return;
}
// Iterate over all resources in the project
IProject project = resource.getProject();
project.accept(myResource -> {
if (myResource.getType() == IResource.FILE && myResource instanceof IFile) {
IFile file = (IFile) myResource;
// Filter to PDE-relevant files only
if (isPDERelevantFile(file)) {
try {
processFile(fixcore, file, operations, compilationUnit);
} catch (Exception e) {
LOG.log(new Status(IStatus.ERROR, PLUGIN_ID,
"Error processing file: " + file.getName(), e));
}
}
}
return true; // Continue iteration
});
} catch (CoreException e) {
LOG.log(new Status(IStatus.ERROR, PLUGIN_ID,
"Error during XML cleanup", e));
}
}
/**
* Check if a file is PDE-relevant based on name, extension, and location.
*
* @param file the file to check
* @return true if the file should be processed
*/
private boolean isPDERelevantFile(IFile file) {
String fileName = file.getName();
String extension = file.getFileExtension();
// Check if it's a known PDE file name
if (PDE_FILE_NAMES.contains(fileName)) {
// Must be in project root, OSGI-INF, or META-INF
return isInPDELocation(file);
}
// Check if it's a PDE extension (exsd, xsd)
if (extension != null && PDE_EXTENSIONS.contains(extension)) {
return isInPDELocation(file);
}
return false;
}
/**
* Check if file is in a PDE-typical location.
*
* @param file the file to check
* @return true if in root, OSGI-INF, or META-INF
*/
private boolean isInPDELocation(IFile file) {
IResource parent = file.getParent();
// Check if in project root
if (parent instanceof IProject) {
return true;
}
// Check if in OSGI-INF or META-INF
if (parent instanceof IFolder) {
String folderName = parent.getName();
if (PDE_DIRECTORIES.contains(folderName)) {
return true;
}
// Also check parent's parent (for nested structures)
IResource grandParent = parent.getParent();
if (grandParent instanceof IFolder) {
if (PDE_DIRECTORIES.contains(grandParent.getName())) {
return true;
}
}
}
return false;
}
/**
* Process a single XML file for cleanup.
*
* @param fixcore the cleanup fix core
* @param file the file to process
* @param operations the set of operations to add to
* @param compilationUnit the compilation unit for creating operations
* @throws Exception if processing fails
*/
private void processFile(XMLCleanUpFixCore fixcore, IFile file,
Set<CompilationUnitRewriteOperation> operations, CompilationUnit compilationUnit)
throws Exception {
// Check if file was already processed
Path filePath = file.getLocation().toFile().toPath();
if (processedFiles.contains(filePath)) {
return;
}
// Read original content
String originalContent;
try (InputStream is = file.getContents()) {
originalContent = new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
// Transform the file
String transformedContent = SchemaTransformationUtils.transform(filePath, enableIndent);
// Only create operation if content actually changed
if (!originalContent.equals(transformedContent)) {
XMLCandidateHit hit = new XMLCandidateHit(file, originalContent);
hit.transformedContent = transformedContent;
// Use the compilation unit as a placeholder for the operation
// This is required by the Eclipse cleanup framework
hit.whileStatement = compilationUnit;
operations.add(fixcore.rewrite(hit));
// Mark file as processed
processedFiles.add(filePath);
LOG.log(new Status(IStatus.INFO, PLUGIN_ID,
"Queued transformation for: " + file.getName()));
}
}
@Override
public void rewrite(XMLCleanUpFixCore upp, final XMLCandidateHit hit,
final CompilationUnitRewrite cuRewrite, TextEditGroup group) {
if (hit.file == null || hit.transformedContent == null) {
LOG.log(new Status(IStatus.WARNING, PLUGIN_ID,
"Invalid XMLCandidateHit: missing file or transformed content"));
return;
}
try {
// Update file contents using Eclipse workspace API
byte[] newContent = hit.transformedContent.getBytes(StandardCharsets.UTF_8);
ByteArrayInputStream inputStream = new ByteArrayInputStream(newContent);
// Update file (don't force, keep history)
hit.file.setContents(inputStream, IResource.KEEP_HISTORY, null);
// Refresh the resource to sync with filesystem
hit.file.refreshLocal(IResource.DEPTH_ZERO, null);
LOG.log(new Status(IStatus.INFO, PLUGIN_ID,
"Applied transformation to: " + hit.file.getName()));
} catch (CoreException e) {
LOG.log(new Status(IStatus.ERROR, PLUGIN_ID,
"Failed to write transformed content to: " + hit.file.getName(), e));
}
}
@Override
public String getPreview(boolean afterRefactoring) {
if (afterRefactoring) {
return """
/* XML Cleanup - After:
* - Empty elements collapsed to self-closing
* - Whitespace optimized
*/
// <?xml version="1.0" encoding="UTF-8"?>
// <plugin>
// <extension point="org.eclipse.ui.views"/>
// <view id="my.view" name="My View"/>
// </plugin>
"""; //$NON-NLS-1$
}
return """
/* XML Cleanup - Before:
* - Empty elements with closing tags
* - Extra whitespace
*/
// <?xml version="1.0" encoding="UTF-8"?>
// <plugin>
// <extension point="org.eclipse.ui.views">
// </extension>
// <view id="my.view" name="My View">
// </view>
// </plugin>
"""; //$NON-NLS-1$
}
}