AbstractExplicitEncoding.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.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.UnionType;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
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.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseExplicitEncodingFixCore;
/**
* Abstract base class for encoding-related quick fixes. Provides common functionality
* for finding and rewriting code patterns that use implicit or string-based encoding
* specifications.
*
* <p>Subclasses must implement:
* <ul>
* <li>{@link #find} - to locate code patterns that need to be fixed</li>
* <li>{@link #rewrite} - to apply the encoding-related transformation</li>
* <li>{@link #getPreview} - to generate preview text for the fix</li>
* </ul>
*
* @param <T> The type of AST node that this encoding handler processes
*/
public abstract class AbstractExplicitEncoding<T extends ASTNode> {
/** Fully qualified name of java.io.UnsupportedEncodingException. */
private static final String JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION = "java.io.UnsupportedEncodingException"; //$NON-NLS-1$
/** Simple name of UnsupportedEncodingException for matching in exception types. */
private static final String UNSUPPORTED_ENCODING_EXCEPTION = "UnsupportedEncodingException"; //$NON-NLS-1$
/**
* Immutable map of standard charset names (e.g., "UTF-8") to their corresponding
* StandardCharsets constant names (e.g., "UTF_8").
* <p>
* This mapping covers the six charsets guaranteed to be available on every Java platform.
* </p>
* @since 1.3
*/
public static final Map<String, String> ENCODING_MAP = Map.of(
"UTF-8", "UTF_8", //$NON-NLS-1$ //$NON-NLS-2$
"UTF-16", "UTF_16", //$NON-NLS-1$ //$NON-NLS-2$
"UTF-16BE", "UTF_16BE", //$NON-NLS-1$ //$NON-NLS-2$
"UTF-16LE", "UTF_16LE", //$NON-NLS-1$ //$NON-NLS-2$
"ISO-8859-1", "ISO_8859_1", //$NON-NLS-1$ //$NON-NLS-2$
"US-ASCII", "US_ASCII" //$NON-NLS-1$ //$NON-NLS-2$
);
/**
* Immutable set of supported encoding names that can be converted to StandardCharsets constants.
* @since 1.3
*/
public static final Set<String> ENCODINGS = ENCODING_MAP.keySet();
/**
* Maps standard charset names (e.g., "UTF-8") to their corresponding
* StandardCharsets constant names (e.g., "UTF_8").
* @deprecated Use {@link #ENCODING_MAP} instead. This field is maintained for backward
* compatibility but is immutable and will throw UnsupportedOperationException
* if modification is attempted.
*/
@Deprecated
static final Map<String, String> encodingmap = ENCODING_MAP;
/**
* Set of supported encoding names that can be converted to StandardCharsets constants.
* @deprecated Use {@link #ENCODINGS} instead. This field is maintained for backward
* compatibility but is immutable and will throw UnsupportedOperationException
* if modification is attempted.
*/
@Deprecated
static final Set<String> encodings = ENCODINGS;
/**
* Immutable record to hold node data for encoding transformations.
* Replaces the mutable Nodedata class for better thread safety and immutability.
*
* @param replace Whether to replace an existing encoding parameter (true) or appended (false)
* @param visited The AST node that was visited and needs modification
* @param encoding The encoding constant name (e.g., "UTF_8"), or null for default charset
*/
protected static record NodeData(boolean replace, ASTNode visited, String encoding) {
}
/**
* Thread-safe map to cache charset constant references during aggregation.
* Used to avoid creating duplicate QualifiedName instances.
*/
private static final Map<String, QualifiedName> CHARSET_CONSTANTS = new ConcurrentHashMap<>();
/**
* Returns the charset constants map for use in encoding transformations.
*
* @return thread-safe map of charset constants
*/
protected static Map<String, QualifiedName> getCharsetConstants() {
return CHARSET_CONSTANTS;
}
/** Key used for storing encoding information in data holders. */
protected static final String KEY_ENCODING = "encoding"; //$NON-NLS-1$
/** Key used for storing replace flag in data holders. */
protected static final String KEY_REPLACE = "replace"; //$NON-NLS-1$
/**
* @deprecated Use {@link #KEY_ENCODING} instead. This field will be removed in a future version.
*/
@Deprecated(forRemoval = true)
protected static final String ENCODING = KEY_ENCODING;
/**
* @deprecated Use {@link #KEY_REPLACE} instead. This field will be removed in a future version.
*/
@Deprecated(forRemoval = true)
protected static final String REPLACE = KEY_REPLACE;
/**
* Finds all occurrences of the encoding pattern that this handler processes
* and adds corresponding rewrite operations.
*
* @param fixcore the fix core instance, must not be null
* @param compilationUnit the compilation unit to search in, must not be null
* @param operations the set to add rewrite operations to, must not be null
* @param nodesprocessed the set of already processed nodes (to avoid duplicates), must not be null
* @param cb the change behavior configuration, must not be null
*/
public abstract void find(UseExplicitEncodingFixCore fixcore, CompilationUnit compilationUnit, Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed, ChangeBehavior cb);
/**
* Rewrites the visited AST node to use explicit encoding.
*
* @param useExplicitEncodingFixCore the fix core instance, must not be null
* @param visited the AST node to rewrite, must not be null
* @param cuRewrite the compilation unit rewrite context, must not be null
* @param group the text edit group for grouping changes, must not be null
* @param cb the change behavior configuration, must not be null
* @param data the reference holder containing node-specific data, must not be null
*/
public abstract void rewrite(UseExplicitEncodingFixCore useExplicitEncodingFixCore, T visited, CompilationUnitRewrite cuRewrite,
TextEditGroup group, ChangeBehavior cb, ReferenceHolder<ASTNode, Object> data);
/**
* Adds an import to the class. This method should be used for every class reference added to
* the generated code.
*
* @param typeName a fully qualified name of a type, must not be null
* @param cuRewrite CompilationUnitRewrite, must not be null
* @param ast AST, must not be null
* @return simple name of a class if the import was added and fully qualified name if there was
* a conflict; never null
*/
protected static Name addImport(String typeName, final CompilationUnitRewrite cuRewrite, AST ast) {
String importedName = cuRewrite.getImportRewrite().addImport(typeName);
return ast.newName(importedName);
}
/**
* Checks if a string literal contains a known encoding that can be converted
* to a StandardCharsets constant.
*
* @param literal the string literal to check, may be null
* @return true if the literal contains a known encoding, false otherwise
*/
protected static boolean isKnownEncoding(StringLiteral literal) {
if (literal == null) {
return false;
}
return ENCODINGS.contains(literal.getLiteralValue().toUpperCase(Locale.ROOT));
}
/**
* Gets the StandardCharsets constant name for a given encoding string literal.
*
* @param literal the string literal containing the encoding name, may be null
* @return the StandardCharsets constant name (e.g., "UTF_8"), or null if the literal
* is null or contains an unknown encoding
*/
protected static String getEncodingConstantName(StringLiteral literal) {
if (literal == null) {
return null;
}
return ENCODING_MAP.get(literal.getLiteralValue().toUpperCase(Locale.ROOT));
}
/**
* Finds the enclosing MethodDeclaration or TypeDeclaration for a given AST node.
*
* @param node the starting node for the search, may be null
* @return the enclosing MethodDeclaration or TypeDeclaration, or null if not found
*/
private static ASTNode findEnclosingMethodOrType(ASTNode node) {
if (node == null) {
return null;
}
ASTNode methodDecl = ASTNodes.getFirstAncestorOrNull(node, MethodDeclaration.class);
ASTNode typeDecl = ASTNodes.getFirstAncestorOrNull(node, TypeDeclaration.class);
// Return the closest ancestor. In Java, methods are always declared inside types,
// so if a MethodDeclaration exists, it is guaranteed to be closer than any TypeDeclaration.
// getFirstAncestorOrNull returns the nearest ancestor of each type, so we just need to
// prefer the more specific (nested) one.
if (methodDecl != null) {
return methodDecl;
}
return typeDecl;
}
/**
* Extracts the string literal value from a variable declaration fragment if its initializer
* is a string literal.
*
* @param fragment the variable declaration fragment to check, must not be null
* @param variableIdentifier the identifier of the variable to match, must not be null
* @return the uppercase string literal value if found, null otherwise
*/
private static String extractStringLiteralValue(VariableDeclarationFragment fragment, String variableIdentifier) {
if (!fragment.getName().getIdentifier().equals(variableIdentifier)) {
return null;
}
Expression initializer = fragment.getInitializer();
if (initializer instanceof StringLiteral) {
return ((StringLiteral) initializer).getLiteralValue().toUpperCase(Locale.ROOT);
}
return null;
}
/**
* Searches for a variable's string literal value within a list of variable declaration fragments.
*
* @param fragments the list of fragments to search in, must not be null
* @param variableIdentifier the identifier of the variable to find, must not be null
* @return the uppercase string literal value if found, null otherwise
*/
private static String findValueInFragments(List<?> fragments, String variableIdentifier) {
for (Object frag : fragments) {
VariableDeclarationFragment fragment = (VariableDeclarationFragment) frag;
String value = extractStringLiteralValue(fragment, variableIdentifier);
if (value != null) {
return value;
}
}
return null;
}
/**
* Searches for a variable's string literal value within method body statements.
*
* @param method the method declaration to search in, must not be null
* @param variableIdentifier the identifier of the variable to find, must not be null
* @return the uppercase string literal value if found, null otherwise
*/
private static String findVariableValueInMethod(MethodDeclaration method, String variableIdentifier) {
Block body = method.getBody();
if (body == null) {
return null;
}
List<?> statements = body.statements();
for (Object stmt : statements) {
if (stmt instanceof VariableDeclarationStatement) {
VariableDeclarationStatement varDeclStmt = (VariableDeclarationStatement) stmt;
String value = findValueInFragments(varDeclStmt.fragments(), variableIdentifier);
if (value != null) {
return value;
}
}
}
return null;
}
/**
* Searches for a variable's string literal value within type field declarations.
*
* @param type the type declaration to search in, must not be null
* @param variableIdentifier the identifier of the variable to find, must not be null
* @return the uppercase string literal value if found, null otherwise
*/
private static String findVariableValueInType(TypeDeclaration type, String variableIdentifier) {
FieldDeclaration[] fields = type.getFields();
for (FieldDeclaration field : fields) {
String value = findValueInFragments(field.fragments(), variableIdentifier);
if (value != null) {
return value;
}
}
return null;
}
/**
* Finds the value of a variable by searching for its declaration in the enclosing
* method or type. This is used to resolve variable references to their string literal
* initializer values.
*
* @param variable the SimpleName representing the variable reference, may be null
* @param context the AST node providing context for the search, may be null
* @return the uppercase string literal value of the variable's initializer,
* or null if the variable is null, context is null, or the value cannot be found
*/
protected static String findVariableValue(SimpleName variable, ASTNode context) {
if (variable == null || context == null) {
return null;
}
ASTNode enclosing = findEnclosingMethodOrType(context);
if (enclosing == null) {
return null;
}
String variableIdentifier = variable.getIdentifier();
if (enclosing instanceof MethodDeclaration) {
return findVariableValueInMethod((MethodDeclaration) enclosing, variableIdentifier);
} else if (enclosing instanceof TypeDeclaration) {
return findVariableValueInType((TypeDeclaration) enclosing, variableIdentifier);
}
return null;
}
/**
* Generates a preview string showing the code before or after the refactoring.
*
* @param afterRefactoring true to show the code after refactoring, false for before
* @param cb the change behavior configuration
* @return the preview string, never null
*/
public abstract String getPreview(boolean afterRefactoring, ChangeBehavior cb);
/**
* Finds the enclosing MethodDeclaration or TryStatement for exception handling removal.
*
* @param node the starting node for the search
* @return the enclosing MethodDeclaration or TryStatement, or null if not found
*/
private static ASTNode findEnclosingMethodOrTry(ASTNode node) {
ASTNode tryStmt = ASTNodes.getFirstAncestorOrNull(node, TryStatement.class);
ASTNode methodDecl = ASTNodes.getFirstAncestorOrNull(node, MethodDeclaration.class);
// Return the closest ancestor. In Java, try statements are always inside method bodies,
// so if a TryStatement exists, it is guaranteed to be closer than any MethodDeclaration.
// getFirstAncestorOrNull returns the nearest ancestor of each type, so we just need to
// prefer the more specific (nested) one.
if (tryStmt != null) {
return tryStmt;
}
return methodDecl;
}
/**
* Checks if a type represents UnsupportedEncodingException.
*
* @param type the type to check
* @return true if the type is UnsupportedEncodingException
*/
private static boolean isUnsupportedEncodingException(Type type) {
return type.toString().equals(UNSUPPORTED_ENCODING_EXCEPTION);
}
/**
* Removes UnsupportedEncodingException from method's throws clause.
*
* @param method the method declaration to modify
* @param rewrite the AST rewrite to use
* @param group the text edit group
* @param importRewriter the import rewrite to use
*/
private static void removeExceptionFromMethodThrows(MethodDeclaration method, ASTRewrite rewrite, TextEditGroup group, ImportRewrite importRewriter) {
ListRewrite throwsRewrite = rewrite.getListRewrite(method, MethodDeclaration.THROWN_EXCEPTION_TYPES_PROPERTY);
List<Type> thrownExceptions = method.thrownExceptionTypes();
for (Type exceptionType : thrownExceptions) {
if (isUnsupportedEncodingException(exceptionType)) {
throwsRewrite.remove(exceptionType, group);
importRewriter.removeImport(JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION);
}
}
}
/**
* Handles UnsupportedEncodingException removal from a union type in a catch clause.
*
* @param unionType the union type to modify
* @param catchClause the catch clause containing the union type
* @param rewrite the AST rewrite to use
* @param group the text edit group
*/
private static void removeExceptionFromUnionType(UnionType unionType, CatchClause catchClause, ASTRewrite rewrite, TextEditGroup group) {
ListRewrite unionRewrite = rewrite.getListRewrite(unionType, UnionType.TYPES_PROPERTY);
List<Type> types = unionType.types();
// Collect types to remove first to avoid modification during iteration
List<Type> typesToRemove = types.stream()
.filter(AbstractExplicitEncoding::isUnsupportedEncodingException)
.toList();
typesToRemove.forEach(type -> unionRewrite.remove(type, group));
// Calculate remaining count after scheduled removals
int remainingCount = types.size() - typesToRemove.size();
if (remainingCount == 1) {
// Find the remaining type (not in removal list)
Type remainingType = types.stream()
.filter(type -> !typesToRemove.contains(type))
.findFirst()
.orElse(null);
if (remainingType != null) {
rewrite.replace(unionType, remainingType, group);
}
} else if (remainingCount == 0) {
rewrite.remove(catchClause, group);
}
}
/**
* Removes UnsupportedEncodingException from catch clauses in a try statement.
*
* @param tryStatement the try statement to process
* @param rewrite the AST rewrite to use
* @param group the text edit group
* @param importRewriter the import rewrite to use
*/
private static void removeExceptionFromTryCatch(TryStatement tryStatement, ASTRewrite rewrite, TextEditGroup group, ImportRewrite importRewriter) {
List<CatchClause> catchClauses = tryStatement.catchClauses();
for (CatchClause catchClause : catchClauses) {
SingleVariableDeclaration exception = catchClause.getException();
Type exceptionType = exception.getType();
if (exceptionType instanceof UnionType) {
removeExceptionFromUnionType((UnionType) exceptionType, catchClause, rewrite, group);
} else if (isUnsupportedEncodingException(exceptionType)) {
rewrite.remove(catchClause, group);
importRewriter.removeImport(JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION);
}
}
}
/**
* Simplifies a try statement that has become empty after removing catch clauses.
*
* @param tryStatement the try statement to check and simplify
* @param rewrite the AST rewrite to use
* @param group the text edit group
*/
private static void simplifyEmptyTryStatement(TryStatement tryStatement, ASTRewrite rewrite, TextEditGroup group) {
if (!tryStatement.catchClauses().isEmpty() || tryStatement.getFinally() != null) {
return;
}
Block tryBlock = tryStatement.getBody();
boolean hasResources = !tryStatement.resources().isEmpty();
boolean hasStatements = !tryBlock.statements().isEmpty();
if (!hasResources && !hasStatements) {
rewrite.remove(tryStatement, group);
} else if (!hasResources) {
rewrite.replace(tryStatement, tryBlock, group);
}
}
/**
* Removes UnsupportedEncodingException from the enclosing method's throws clause
* or from catch clauses in a try statement. This is called after converting string-based
* encoding to StandardCharsets, since StandardCharsets methods don't throw
* UnsupportedEncodingException.
*
* <p>For method declarations, removes UnsupportedEncodingException from the throws clause.
* For try statements, removes catch clauses that only catch UnsupportedEncodingException,
* or removes the exception type from union types in multi-catch clauses.
*
* @param visited the AST node that was modified, must not be null
* @param group the text edit group for tracking changes, must not be null
* @param rewrite the AST rewrite context, must not be null
* @param importRewriter the import rewrite for removing unused imports, must not be null
*/
protected void removeUnsupportedEncodingException(final ASTNode visited, TextEditGroup group, ASTRewrite rewrite, ImportRewrite importRewriter) {
ASTNode parent = findEnclosingMethodOrTry(visited);
if (parent == null) {
return;
}
if (parent instanceof MethodDeclaration) {
removeExceptionFromMethodThrows((MethodDeclaration) parent, rewrite, group, importRewriter);
} else if (parent instanceof TryStatement) {
TryStatement tryStatement = (TryStatement) parent;
removeExceptionFromTryCatch(tryStatement, rewrite, group, importRewriter);
simplifyEmptyTryStatement(tryStatement, rewrite, group);
}
}
}