/*
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.eclipse.refactoring.actions;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.eclipse.codebrowsing.fragments.ASTFragmentKind;
import org.codehaus.groovy.eclipse.codebrowsing.fragments.IASTFragment;
import org.codehaus.groovy.eclipse.codebrowsing.requestor.ASTNodeFinder;
import org.codehaus.groovy.eclipse.codebrowsing.requestor.Region;
import org.codehaus.groovy.eclipse.codebrowsing.selection.FindSurroundingNode;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.codehaus.jdt.groovy.model.ModuleNodeMapper.ModuleNodeInfo;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.TypeNameMatch;
import org.eclipse.jdt.groovy.core.util.GroovyUtils;
import org.eclipse.jdt.groovy.core.util.ReflectionUtils;
import org.eclipse.jdt.internal.corext.CorextMessages;
import org.eclipse.jdt.internal.corext.ValidateEditException;
import org.eclipse.jdt.internal.corext.codemanipulation.AddImportsOperation.IChooseImportQuery;
import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationMessages;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.corext.util.Resources;
import org.eclipse.jdt.internal.corext.util.TypeNameMatchCollector;
import org.eclipse.jdt.internal.ui.JavaUIStatus;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
import org.eclipse.jdt.internal.ui.viewsupport.BasicElementLabels;
import org.eclipse.jdt.ui.CodeStyleConfiguration;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.RangeMarker;
import org.eclipse.text.edits.TextEdit;
public class AddImportOnSelectionAction extends AddImportOnSelectionAdapter {
public AddImportOnSelectionAction(CompilationUnitEditor editor) {
super(editor);
}
protected AddImportOperation newAddImportOperation(final GroovyCompilationUnit compilationUnit, final ITextSelection textSelection, final IChooseImportQuery typeQuery) {
return new AddImportOperation() {
private IStatus fStatus = Status.OK_STATUS;
public IStatus getStatus() {
return fStatus;
}
public void run(IProgressMonitor monitor) throws CoreException {
SubMonitor submon = SubMonitor.convert(monitor, CodeGenerationMessages.AddImportsOperation_description, 4);
try {
ModuleNodeInfo info = compilationUnit.getModuleInfo(true);
if (info.isEmpty()) {
fStatus = Status.CANCEL_STATUS;
return;
}
submon.worked(1);
ImportRewrite importRewrite = CodeStyleConfiguration.createImportRewrite(compilationUnit, true);
TextEdit edit = evaluateEdits(info.module, importRewrite, submon.newChild(1));
if (edit == null) {
return;
}
MultiTextEdit result = new MultiTextEdit();
result.addChild(edit);
result.addChild(importRewrite.rewriteImports(submon.newChild(1)));
applyEdit(compilationUnit, result, true, submon.newChild(1));
} catch (OperationCanceledException cancel) {
if (fStatus == Status.OK_STATUS)
fStatus = Status.CANCEL_STATUS;
} finally {
submon.done();
}
}
private TextEdit evaluateEdits(ModuleNode moduleNode, ImportRewrite importRewrite, IProgressMonitor monitor) throws CoreException {
Region selectRegion = new Region(textSelection.getOffset(), textSelection.getLength());
ASTNodeFinder nodeFinder = new ASTNodeFinder(selectRegion);
ASTNode node = nodeFinder.doVisit(moduleNode);
if (node != null) {
if (node instanceof VariableExpression) {
// part of object expression "Pattern p = ..." or part of init expression "def p = Pattern.compile(...)"
TypeNameMatch choice = findCandidateTypes(((VariableExpression) node).getName(), monitor);
importRewrite.addImport(choice.getFullyQualifiedName());
return new RangeMarker(node.getStart(), node.getLength());
}
if (node instanceof ClassNode || node instanceof ClassExpression || node instanceof ConstructorCallExpression) {
// simple name like "List", partially-qualified name like "Map.Entry", or fully-qualified name like "java.util.regex.Pattern"
ClassNode type = componentType(node);
int typeStart = startOffset(!(node instanceof ConstructorCallExpression) ? node : type, nodeFinder);
if (moduleNode.getClasses().contains(type)) {
return null; // skip type in same unit
}
// check for unknown and unqualified type name
if (type.getName().equals(type.getNameWithoutPackage())) {
TypeNameMatch choice = findCandidateTypes(type.getName(), monitor);
importRewrite.addImport(choice.getFullyQualifiedName());
return new RangeMarker(typeStart, type.getName().length());
}
// check for known but unqualified type name -- could be imported (explicit, on-demand, default, alias) or in the same package
String simple = type.getNameWithoutPackage().substring(type.getNameWithoutPackage().lastIndexOf('$') + 1);
String prefix = compilationUnit.getSource().substring(typeStart, selectRegion.getEnd());
if (simple.startsWith(prefix) && prefix.length() > 0) {
importRewrite.addImport(type.getName()); // redundant but user requested
return new RangeMarker(typeStart, type.getNameWithoutPackage().length());
}
// check for selection on the type's name or qualifier string
String source = compilationUnit.getSource().substring(typeStart, endOffset(node, nodeFinder));
int nameStart = typeStart + source.indexOf(GroovyUtils.splitName(type)[1]);
if (nameStart > typeStart) {
if (nameStart <= selectRegion.getEnd()) {
String result = importRewrite.addImport(type.getName().replace('$', '.'));
// result is fully-qualified name in case of conflict with another import
if (result.indexOf('.') > 0) {
fStatus = JavaUIStatus.createError(IStatus.ERROR,
CodeGenerationMessages.AddImportsOperation_error_importclash, null);
return null;
}
return new DeleteEdit(typeStart, nameStart - typeStart);
}
Pattern pattern;
Matcher matcher;
String qualifier = GroovyUtils.splitName(type)[0].replace('$', '.');
if (prefix.length() > 0) {
// check for selection in fully-qualified name like 'java.lang.String' or 'java.util.Map.Entry'
pattern = Pattern.compile("^\\Q" + prefix + "\\E\\w*");
matcher = pattern.matcher(qualifier);
if (matcher.find()) {
IType it = compilationUnit.getJavaProject().findType(matcher.group());
if (it == null) return null; // selected 'java.lang' or whatever
// selected 'java.util.Map' or similar
importRewrite.addImport(matcher.group());
return new DeleteEdit(typeStart, endOffsetMinus(selectRegion.getEnd()) - typeStart);
}
}
// expand prefix to include the complete identifier segment
prefix = compilationUnit.getSource().substring(typeStart, endOffsetPlus(selectRegion.getEnd()));
// check for selection in partially-qualified name like 'Map.Entry'
pattern = Pattern.compile("\\b\\Q" + prefix + "\\E$");
matcher = pattern.matcher(qualifier);
if (matcher.find()) {
importRewrite.addImport(qualifier);
// TODO: Is there ever a reason to delete anything?
return new RangeMarker(typeStart, nameStart - typeStart);
}
}
}
if (node instanceof ConstantExpression) {
// static references like "TimeUnit.SECONDS" or "Pattern.compile(...)"
IASTFragment fragment = new FindSurroundingNode(new Region(node)).doVisitSurroundingNode(moduleNode);
if (fragment.kind() == ASTFragmentKind.PROPERTY) {
Expression expr = fragment.getAssociatedExpression();
if (expr instanceof ClassExpression) {
importRewrite.addStaticImport(expr.getType().getName().replace('$', '.'), node.getText(), true);
return new DeleteEdit(expr.getStart(), expr.getLength() + 1);
}
if (expr instanceof VariableExpression) {
TypeNameMatch choice = findCandidateTypes(((VariableExpression) expr).getName(), monitor);
importRewrite.addStaticImport(choice.getFullyQualifiedName(), node.getText(), true);
return new DeleteEdit(expr.getStart(), expr.getLength() + 1);
}
}
if (fragment.kind() == ASTFragmentKind.METHOD_CALL) {
MethodCallExpression call = (MethodCallExpression) fragment.getAssociatedNode();
if (call != null && !call.isUsingGenerics()) {
Expression expr = call.getObjectExpression();
if (expr instanceof ClassExpression) {
importRewrite.addStaticImport(expr.getType().getName().replace('$', '.'), call.getMethodAsString(), false);
return new DeleteEdit(expr.getStart(), call.getMethod().getStart() - expr.getStart());
}
if (expr instanceof VariableExpression) {
TypeNameMatch choice = findCandidateTypes(((VariableExpression) expr).getName(), monitor);
importRewrite.addStaticImport(choice.getFullyQualifiedName(), call.getMethodAsString(), false);
return new DeleteEdit(expr.getStart(), call.getMethod().getStart() - expr.getStart());
}
}
}
}
}
return null;
}
private TypeNameMatch findCandidateTypes(String typeName, IProgressMonitor monitor) throws CoreException {
int matchRule = SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
searchFor = IJavaSearchConstants.TYPE;
List<TypeNameMatch> typesFound = new ArrayList<TypeNameMatch>();
new SearchEngine().searchAllTypeNames(null, 0, typeName.toCharArray(), matchRule, searchFor,
SearchEngine.createJavaSearchScope(new IJavaElement[] {compilationUnit.getJavaProject()}),
new TypeNameMatchCollector(typesFound), IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, monitor);
if (typesFound.isEmpty()) {
fStatus = JavaUIStatus.createError(IStatus.ERROR, Messages.format(
CodeGenerationMessages.AddImportsOperation_error_notresolved_message,
BasicElementLabels.getJavaElementName(typeName)), null);
throw new OperationCanceledException();
}
TypeNameMatch choice = typeQuery.chooseImport(typesFound.toArray(new TypeNameMatch[typesFound.size()]), typeName);
if (choice == null) throw new OperationCanceledException();
return choice;
}
private int startOffset(ASTNode node, ASTNodeFinder nodeFinder) throws CoreException {
int start = node.getStart();
if (node.getEnd() < 1) {
Region nodeRegion = (Region) ReflectionUtils.getPrivateField(ASTNodeFinder.class, "sloc", nodeFinder);
if (nodeRegion != null) {
start = nodeRegion.getOffset(); // may be approximate
while (!Character.isJavaIdentifierStart(compilationUnit.getSource().charAt(start))) {
start += 1;
}
}
}
return start;
}
private int endOffset(ASTNode node, ASTNodeFinder nodeFinder) throws CoreException {
int end = node.getEnd();
if (end < 1) {
Region nodeRegion = (Region) ReflectionUtils.getPrivateField(ASTNodeFinder.class, "sloc", nodeFinder);
if (nodeRegion != null) {
end = nodeRegion.getEnd(); // may be approximate
while (end > 0 && !Character.isJavaIdentifierPart(compilationUnit.getSource().charAt(end - 1))) {
end -= 1;
}
}
}
return end;
}
private int endOffsetPlus(int end) throws CoreException {
while (Character.isJavaIdentifierPart(compilationUnit.getSource().charAt(end))) { end += 1; }
return end;
}
private int endOffsetMinus(int end) throws CoreException {
while (end > 0 && Character.isJavaIdentifierPart(compilationUnit.getSource().charAt(end - 1))) { end -= 1; }
return end;
}
private ClassNode componentType(ASTNode node) {
ClassNode type = (node instanceof ClassNode ? (ClassNode) node : ((Expression) node).getType());
return type.getComponentType() != null ? componentType(type.getComponentType()) : type;
}
};
}
/**
* Applies a text edit to a compilation unit.
*
* @param cu the compilation unit to apply the edit to
* @param edit the edit to apply
* @param save if set, save the CU after the edit has been applied
* @param monitor the progress monitor to use
* @throws CoreException Thrown when the access to the CU failed
* @throws ValidateEditException if validate edit fails
*/
// copied from JavaModelUtil (moved to JavaElementUtil circa Eclipse 4.7)
protected static void applyEdit(ICompilationUnit cu, TextEdit edit, boolean save, IProgressMonitor monitor) throws CoreException, ValidateEditException {
SubMonitor subMonitor = SubMonitor.convert(monitor, CorextMessages.JavaModelUtil_applyedit_operation, 2);
IFile file = (IFile) cu.getResource();
if (!save || !file.exists()) {
cu.applyTextEdit(edit, subMonitor.newChild(2));
} else {
IStatus status = Resources.makeCommittable(file, null);
if (!status.isOK()) {
throw new ValidateEditException(status);
}
cu.applyTextEdit(edit, subMonitor.newChild(1));
cu.save(subMonitor.newChild(1), true);
}
}
}