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.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
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.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
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.SimpleType;
import org.eclipse.jdt.core.dom.Statement;
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.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
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.jdt.internal.corext.refactoring.structure.ImportRemover;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.UseExplicitEncodingFixCore;
import org.sandbox.jdt.internal.corext.util.ImportUtils;
import org.sandbox.jdt.triggerpattern.cleanup.ExceptionCleanupHelper;
/**
* Shared support for cleanups that replace implicit or string-based encodings.
*
* @param <T> AST node handled by the concrete cleanup
*/
public abstract class AbstractExplicitEncoding<T extends ASTNode> {
private static final String JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION = "java.io.UnsupportedEncodingException"; //$NON-NLS-1$
private static final String UNSUPPORTED_ENCODING_EXCEPTION = "UnsupportedEncodingException"; //$NON-NLS-1$
private static final String REMOVED_UNSUPPORTED_ENCODING_CATCHES_PROPERTY =
AbstractExplicitEncoding.class.getName() + ".removedUnsupportedEncodingCatches"; //$NON-NLS-1$
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$
public static final Set<String> ENCODINGS = ENCODING_MAP.keySet();
@Deprecated
static final Map<String, String> encodingmap = ENCODING_MAP;
@Deprecated
static final Set<String> encodings = ENCODINGS;
protected static record NodeData(boolean replace, ASTNode visited, String encoding) {
}
private static final Map<String, QualifiedName> CHARSET_CONSTANTS = new ConcurrentHashMap<>();
protected static Map<String, QualifiedName> getCharsetConstants() {
return CHARSET_CONSTANTS;
}
protected static final String KEY_ENCODING = "encoding"; //$NON-NLS-1$
protected static final String KEY_REPLACE = "replace"; //$NON-NLS-1$
@Deprecated(forRemoval = true)
protected static final String ENCODING = KEY_ENCODING;
@Deprecated(forRemoval = true)
protected static final String REPLACE = KEY_REPLACE;
public abstract void find(UseExplicitEncodingFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperation> operations, Set<ASTNode> nodesprocessed, ChangeBehavior cb);
public abstract void rewrite(UseExplicitEncodingFixCore useExplicitEncodingFixCore, T visited,
CompilationUnitRewrite cuRewrite, TextEditGroup group, ChangeBehavior cb,
ReferenceHolder<ASTNode, Object> data);
public abstract String getPreview(boolean afterRefactoring, ChangeBehavior cb);
protected static Name addImport(String typeName, CompilationUnitRewrite cuRewrite, AST ast) {
return ImportUtils.addImport(typeName, cuRewrite.getImportRewrite(), ast);
}
protected static boolean isKnownEncoding(StringLiteral literal) {
return literal != null && ENCODINGS.contains(literal.getLiteralValue().toUpperCase(Locale.ROOT));
}
protected static String getEncodingConstantName(StringLiteral literal) {
return literal == null ? null : ENCODING_MAP.get(literal.getLiteralValue().toUpperCase(Locale.ROOT));
}
protected static String getEncodingValue(ASTNode encodingArg, MethodInvocation context) {
if (encodingArg instanceof StringLiteral literal) {
return literal.getLiteralValue().toUpperCase(Locale.ROOT);
}
if (encodingArg instanceof SimpleName simpleName) {
return findVariableValue(simpleName, context);
}
if (encodingArg instanceof QualifiedName qualifiedName) {
return extractStandardCharsetName(qualifiedName);
}
if (encodingArg instanceof FieldAccess fieldAccess) {
return extractStandardCharsetName(fieldAccess);
}
return null;
}
protected static String extractStandardCharsetName(QualifiedName qualifiedName) {
String qualifier = qualifiedName.getQualifier().toString();
if ("StandardCharsets".equals(qualifier) || qualifier.endsWith(".StandardCharsets")) { //$NON-NLS-1$ //$NON-NLS-2$
return qualifiedName.getName().getIdentifier().replace('_', '-');
}
return null;
}
protected static String extractStandardCharsetName(FieldAccess fieldAccess) {
String expression = fieldAccess.getExpression().toString();
if ("StandardCharsets".equals(expression) || expression.endsWith(".StandardCharsets")) { //$NON-NLS-1$ //$NON-NLS-2$
return fieldAccess.getName().getIdentifier().replace('_', '-');
}
return null;
}
private static ASTNode findEnclosingMethodOrType(ASTNode node) {
if (node == null) {
return null;
}
MethodDeclaration method = ASTNodes.getFirstAncestorOrNull(node, MethodDeclaration.class);
return method != null ? method : ASTNodes.getFirstAncestorOrNull(node, TypeDeclaration.class);
}
private static String extractStringLiteralValue(VariableDeclarationFragment fragment,
String variableIdentifier) {
if (!fragment.getName().getIdentifier().equals(variableIdentifier)) {
return null;
}
Expression initializer = fragment.getInitializer();
return initializer instanceof StringLiteral literal
? literal.getLiteralValue().toUpperCase(Locale.ROOT)
: null;
}
private static String findValueInFragments(List<?> fragments, String variableIdentifier) {
for (Object fragmentObject : fragments) {
String value = extractStringLiteralValue((VariableDeclarationFragment) fragmentObject,
variableIdentifier);
if (value != null) {
return value;
}
}
return null;
}
private static String findVariableValueInMethod(MethodDeclaration method, String variableIdentifier) {
Block body = method.getBody();
if (body == null) {
return null;
}
for (Object statement : body.statements()) {
if (statement instanceof VariableDeclarationStatement declaration) {
String value = findValueInFragments(declaration.fragments(), variableIdentifier);
if (value != null) {
return value;
}
}
}
return null;
}
private static String findVariableValueInType(TypeDeclaration type, String variableIdentifier) {
for (FieldDeclaration field : type.getFields()) {
String value = findValueInFragments(field.fragments(), variableIdentifier);
if (value != null) {
return value;
}
}
return null;
}
protected static String findVariableValue(SimpleName variable, ASTNode context) {
if (variable == null || context == null) {
return null;
}
ASTNode enclosing = findEnclosingMethodOrType(context);
if (enclosing instanceof MethodDeclaration method) {
return findVariableValueInMethod(method, variable.getIdentifier());
}
if (enclosing instanceof TypeDeclaration type) {
return findVariableValueInType(type, variable.getIdentifier());
}
return null;
}
private static final Pattern LAST_NLS_COMMENT = Pattern.compile(
"[ ]*\\/\\/\\$NON-NLS-[0-9]+\\$(?!.*\\/\\/\\$NON-NLS-)"); //$NON-NLS-1$
protected static boolean replaceArgumentAndRemoveNLS(ASTRewrite rewrite, ASTNode visited,
ASTNode replacement, TextEditGroup group, CompilationUnitRewrite cuRewrite) {
ASTNode statement = ASTNodes.getFirstAncestorOrNull(visited, Statement.class, FieldDeclaration.class);
if (statement != null && isInsideTryBodyWithOnlyUnsupportedEncodingCatch(statement)) {
return replaceTryBodyAndUnwrap(rewrite, visited, replacement, statement, group, cuRewrite);
}
if (statement == null) {
rewrite.replace(visited, replacement, group);
return false;
}
try {
String buffer = cuRewrite.getCu().getBuffer().getContents();
CompilationUnit root = (CompilationUnit) statement.getRoot();
int start = root.getExtendedStartPosition(statement);
String source = buffer.substring(start, start + root.getExtendedLength(statement));
source = normalizeReplacementSource(source);
String visitedSource = buffer.substring(visited.getStartPosition(),
visited.getStartPosition() + visited.getLength());
String replacementSource = replacement.toString().replaceAll(",", ", "); //$NON-NLS-1$ //$NON-NLS-2$
ASTNode placeholder = rewrite.createStringPlaceholder(
source.replace(visitedSource, replacementSource), statement.getNodeType());
rewrite.replace(statement, placeholder, group);
} catch (JavaModelException exception) {
rewrite.replace(visited, replacement, group);
}
return false;
}
private static boolean replaceTryBodyAndUnwrap(ASTRewrite rewrite, ASTNode visited,
ASTNode replacement, ASTNode statement, TextEditGroup group, CompilationUnitRewrite cuRewrite) {
Block tryBody = (Block) statement.getParent();
TryStatement tryStatement = (TryStatement) tryBody.getParent();
if (!(tryStatement.getParent() instanceof Block parentBlock)) {
rewrite.replace(visited, replacement, group);
return false;
}
try {
String buffer = cuRewrite.getCu().getBuffer().getContents();
CompilationUnit root = (CompilationUnit) statement.getRoot();
String visitedSource = buffer.substring(visited.getStartPosition(),
visited.getStartPosition() + visited.getLength());
String replacementSource = replacement.toString().replaceAll(",", ", "); //$NON-NLS-1$ //$NON-NLS-2$
ListRewrite parentStatements = rewrite.getListRewrite(parentBlock, Block.STATEMENTS_PROPERTY);
List<?> bodyStatements = tryBody.statements();
for (int index = bodyStatements.size() - 1; index >= 0; index--) {
ASTNode bodyStatement = (ASTNode) bodyStatements.get(index);
ASTNode inlinedStatement;
if (bodyStatement == statement) {
int start = root.getExtendedStartPosition(bodyStatement);
String source = buffer.substring(start, start + root.getExtendedLength(bodyStatement));
source = normalizeReplacementSource(source).replace(visitedSource, replacementSource);
inlinedStatement = rewrite.createStringPlaceholder(source, bodyStatement.getNodeType());
} else {
inlinedStatement = rewrite.createMoveTarget(bodyStatement);
}
parentStatements.insertAfter(inlinedStatement, tryStatement, group);
}
CatchClause removedCatch = (CatchClause) tryStatement.catchClauses().get(0);
Set<CatchClause> removedCatches = removedUnsupportedEncodingCatches(root);
removedCatches.add(removedCatch);
cuRewrite.getImportRemover().registerRemovedNode(removedCatch);
if (!hasSurvivingUnsupportedEncodingExceptionReference(root, removedCatches)) {
cuRewrite.getImportRewrite().removeImport(JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION);
}
rewrite.remove(tryStatement, group);
return true;
} catch (JavaModelException exception) {
rewrite.replace(visited, replacement, group);
return false;
}
}
private static String normalizeReplacementSource(String source) {
String normalized = LAST_NLS_COMMENT.matcher(source).replaceFirst(""); //$NON-NLS-1$
normalized = Pattern.compile("^[ \\t]*").matcher(normalized).replaceAll(""); //$NON-NLS-1$ //$NON-NLS-2$
return Pattern.compile("\n[ \\t]*").matcher(normalized).replaceAll("\n"); //$NON-NLS-1$ //$NON-NLS-2$
}
@SuppressWarnings("unchecked")
private static Set<CatchClause> removedUnsupportedEncodingCatches(CompilationUnit root) {
Object stored = root.getProperty(REMOVED_UNSUPPORTED_ENCODING_CATCHES_PROPERTY);
if (stored instanceof Set<?>) {
return (Set<CatchClause>) stored;
}
Set<CatchClause> removedCatches = Collections.newSetFromMap(new IdentityHashMap<>());
root.setProperty(REMOVED_UNSUPPORTED_ENCODING_CATCHES_PROPERTY, removedCatches);
return removedCatches;
}
private static boolean hasSurvivingUnsupportedEncodingExceptionReference(CompilationUnit root,
Set<CatchClause> removedCatches) {
boolean[] found = { false };
root.accept(new ASTVisitor() {
@Override
public boolean visit(SimpleType node) {
if (!isDescendantOfAny(node, removedCatches)
&& node.getName() instanceof SimpleName simpleName
&& UNSUPPORTED_ENCODING_EXCEPTION.equals(simpleName.getIdentifier())) {
found[0] = true;
return false;
}
return !found[0];
}
});
return found[0];
}
private static boolean isDescendantOfAny(ASTNode node, Set<CatchClause> ancestors) {
for (ASTNode current = node; current != null; current = current.getParent()) {
if (ancestors.contains(current)) {
return true;
}
}
return false;
}
private static boolean isInsideTryBodyWithOnlyUnsupportedEncodingCatch(ASTNode statement) {
if (!(statement.getParent() instanceof Block block)
|| !(block.getParent() instanceof TryStatement tryStatement)
|| tryStatement.getBody() != block
|| !tryStatement.resources().isEmpty()
|| tryStatement.getFinally() != null) {
return false;
}
@SuppressWarnings("unchecked")
List<CatchClause> catchClauses = tryStatement.catchClauses();
if (catchClauses.size() != 1) {
return false;
}
Type exceptionType = catchClauses.get(0).getException().getType();
ITypeBinding binding = exceptionType.resolveBinding();
if (binding != null) {
return JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION.equals(binding.getErasure().getQualifiedName());
}
String source = exceptionType.toString();
return UNSUPPORTED_ENCODING_EXCEPTION.equals(source)
|| JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION.equals(source)
|| source.endsWith("." + UNSUPPORTED_ENCODING_EXCEPTION); //$NON-NLS-1$
}
protected void removeUnsupportedEncodingException(ASTNode visited, TextEditGroup group,
ASTRewrite rewrite, ImportRemover importRemover) {
ExceptionCleanupHelper.removeCheckedException(visited,
JAVA_IO_UNSUPPORTED_ENCODING_EXCEPTION,
UNSUPPORTED_ENCODING_EXCEPTION,
group, rewrite, importRemover);
}
}