blob: 5e86a681120f72bddadebdd209330ee7f32e1742 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.refactoring.java.ui;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.swing.JOptionPane;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.api.fileinfo.NonRecursiveFolder;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.*;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.refactoring.java.RefactoringUtils;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.spi.ui.RefactoringUI;
import org.netbeans.modules.refactoring.spi.ui.UI;
import org.openide.ErrorManager;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;
/**
* TODO: should be API
* @author Jan Becicka
*/
public final class ContextAnalyzer {
/**
* create analyze task.
* @param context context of refactoring
* @param factory for creating of RefactoringUI
* @return
*/
public static Runnable createTask(Lookup context, final JavaRefactoringUIFactory factory) {
Runnable task;
EditorCookie ec = context.lookup(EditorCookie.class);
if (RefactoringUtils.isFromEditor(ec)) {
task = new TextComponentTask(ec) {
@Override
protected RefactoringUI createRefactoringUI(TreePathHandle selectedElement, int startOffset, int endOffset, final CompilationInfo info) {
TreePathHandle[] handles;
FileObject[] file;
if(selectedElement != null) {
handles = new TreePathHandle[]{selectedElement};
file = new FileObject[]{selectedElement.getFileObject()};
} else {
handles = new TreePathHandle[0];
file = new FileObject[]{info.getFileObject()};
}
return factory.create(info, handles, file, new NonRecursiveFolder[0]);
}
};
} else if (nodeHandle(context)) {
task = new TreePathHandleTask(new HashSet<Node>(context.lookupAll(Node.class)), false) {
RefactoringUI ui;
private boolean created;
@Override
protected void treePathHandleResolved(TreePathHandle handle, CompilationInfo javac) {
if (size()==1) {
ui = factory.create(javac, new TreePathHandle[]{handle}, null, new NonRecursiveFolder[0]);
created = true;
}
}
@Override
protected RefactoringUI createRefactoringUI(Collection<TreePathHandle> handles) {
if (!created) {
ui = factory.create(null, handles.toArray(new TreePathHandle[handles.size()]), null, new NonRecursiveFolder[0]);
}
return ui;
}
};
} else {
task = new NodeToFileObjectTask(new HashSet<Node>(context.lookupAll(Node.class))) {
RefactoringUI ui;
private boolean created = false;
@Override
protected void nodeTranslated(Node node, Collection<TreePathHandle> handles, CompilationInfo javac) {
if (size()==1) {
ui = factory.create(javac, handles.toArray(new TreePathHandle[1]), new FileObject[]{handles.iterator().next().getFileObject()}, new NonRecursiveFolder[0]);
created = true;
}
}
@Override
protected RefactoringUI createRefactoringUI(FileObject[] selectedElements, Collection<TreePathHandle> handles) {
if (!created) {
if(handles.isEmpty() && selectedElements.length > 0 && "package-info".equals(selectedElements[0].getName())) {
ui = factory.create(null, new TreePathHandle[]{}, selectedElements, new NonRecursiveFolder[0]);
} else {
ui = factory.create(null, handles.toArray(new TreePathHandle[handles.size()]), selectedElements, pkg.toArray(new NonRecursiveFolder[pkg.size()]));
}
}
return ui;
}
};
}
return task;
}
/**
* utility method to perform enable/disable logic for refactoring actions
* @param lookup
* @param notOnlyFile action is disabled when the selection is on File
* @param onlyFromEditor action is enabled only in editor
* @return
*/
public static boolean canRefactorSingle(Lookup lookup, boolean notOnlyFile, boolean onlyFromEditor) {
Collection<? extends Node> nodes = new HashSet<Node>(lookup.lookupAll(Node.class));
if(nodes.size() != 1) {
return false;
}
Node node = nodes.iterator().next();
TreePathHandle tph = node.getLookup().lookup(TreePathHandle.class);
if (tph != null) {
if(JavaRefactoringUtils.isRefactorable(tph.getFileObject())) {
return !onlyFromEditor || RefactoringUtils.isFromEditor(lookup.lookup(EditorCookie.class));
} else {
return false;
}
}
DataObject dObj = node.getLookup().lookup(DataObject.class);
if(null == dObj) {
return false;
}
FileObject fileObj = dObj.getPrimaryFile();
if(null == fileObj || !JavaRefactoringUtils.isRefactorable(fileObj)) {
return false;
}
EditorCookie ec = lookup.lookup(EditorCookie.class);
if (RefactoringUtils.isFromEditor(ec)) {
return true;
}
return !notOnlyFile;
}
private static abstract class TreePathHandleTask implements Runnable, CancellableTask<CompilationController> {
private Collection<TreePathHandle> handles = new ArrayList<TreePathHandle>();
private TreePathHandle current;
private FileObject file;
boolean renameFile;
public TreePathHandleTask(Collection<? extends Node> nodes) {
this(nodes, false);
}
public int size() {
return handles.size();
}
public TreePathHandleTask(Collection<? extends Node> nodes, boolean useFirstHandle) {
for (Node n:nodes) {
TreePathHandle temp = n.getLookup().lookup(TreePathHandle.class);
if (temp!=null) {
handles.add(temp);
if (useFirstHandle) {
break;
}
}
}
Node n = nodes.iterator().next();
DataObject dob = n.getLookup().lookup(DataObject.class);
file = dob != null ? dob.getPrimaryFile() : null;
}
public TreePathHandleTask(TreePathHandle tph) {
handles.add(tph);
}
@Override
public void cancel() {
}
@Override
public void run(CompilationController info) throws Exception {
info.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
Element el = current.resolveElement(info);
if (el!=null && el instanceof TypeElement && !((TypeElement)el).getNestingKind().isNested()) {
if (info.getFileObject().getName().equals(el.getSimpleName().toString())) {
renameFile = true;
}
}
treePathHandleResolved(current, info);
}
@Override
public void run() {
for (TreePathHandle handle:handles) {
FileObject f = handle.getFileObject();
if (f==null) {
if(file != null) {
f = file;
} else {
//ugly workaround for #205142
TopComponent top = (TopComponent) EditorRegistry.lastFocusedComponent().getParent().getParent().getParent().getParent();
f = top.getLookup().lookup(FileObject.class);
}
}
current = handle;
JavaSource source = JavaSource.forFileObject(f);
assert source != null;
try {
source.runUserActionTask(this, true);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
TopComponent activetc = TopComponent.getRegistry().getActivated();
RefactoringUI ui = createRefactoringUI(handles);
if (ui!=null) {
UI.openRefactoringUI(ui, activetc);
} else {
JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_CannotRenameKeyword"));
}
}
/**
* This is the place where subclasses may collect info about handles.
* @param handle handle
* @param javac context of running transaction
*/
protected void treePathHandleResolved(TreePathHandle handle, CompilationInfo javac) {
}
protected abstract RefactoringUI createRefactoringUI(Collection<TreePathHandle> handles);
}
private static abstract class TextComponentTask implements Runnable, CancellableTask<CompilationController> {
private JTextComponent textC;
private int caret;
private int start;
private int end;
private RefactoringUI ui;
private boolean selection;
public TextComponentTask(EditorCookie ec) {
this.textC = ec.getOpenedPanes()[0];
this.caret = textC.getCaretPosition();
this.start = textC.getSelectionStart();
this.end = textC.getSelectionEnd();
this.selection = start != end && (start != -1 || end != -1);
assert caret != -1;
assert start != -1;
assert end != -1;
}
@Override
public void cancel() {
}
@Override
public void run(final CompilationController cc) throws Exception {
cc.toPhase(JavaSource.Phase.RESOLVED);
final int c = selection?start:this.caret;
final TreePath[] selectedElement = new TreePath[] {null};
// final boolean[] insideJavadoc = {false};
final Document doc = cc.getDocument();
doc.render(new Runnable() {
@Override
public void run() {
selectedElement[0] = validateSelection(cc, start, end);
if (selectedElement[0] == null) {
TokenSequence<JavaTokenId> ts = SourceUtils.getJavaTokenSequence(cc.getTokenHierarchy(), c);
int adjustedCaret = c;
ts.move(c);
if (ts.moveNext() && ts.token() != null) {
if (ts.token().id() == JavaTokenId.IDENTIFIER) {
adjustedCaret = ts.offset() + ts.token().length() / 2 + 1;
} /*else if (ts.token().id() == JavaTokenId.JAVADOC_COMMENT) {
TokenSequence<JavadocTokenId> jdts = ts.embedded(JavadocTokenId.language());
if (jdts != null && JavadocImports.isInsideReference(jdts, caret)) {
jdts.move(caret);
if (jdts.moveNext() && jdts.token().id() == JavadocTokenId.IDENT) {
adjustedCaret[0] = jdts.offset();
insideJavadoc[0] = true;
}
} else if (jdts != null && JavadocImports.isInsideParamName(jdts, caret)) {
jdts.move(caret);
if (jdts.moveNext()) {
adjustedCaret[0] = jdts.offset();
insideJavadoc[0] = true;
}
}
}*/
}
selectedElement[0] = cc.getTreeUtilities().pathFor(adjustedCaret);
}
}
});
//workaround for issue 89064
if (selectedElement[0].getLeaf().getKind() == Tree.Kind.COMPILATION_UNIT) {
List<? extends Tree> decls = cc.getCompilationUnit().getTypeDecls();
if (!decls.isEmpty()) {
TreePath path = TreePath.getPath(cc.getCompilationUnit(), decls.get(0));
if (path!=null && cc.getTrees().getElement(path)!=null) {
selectedElement[0] = path;
}
} else {
selectedElement[0] = null;
}
}
ui = createRefactoringUI(selectedElement[0] != null ? TreePathHandle.create(selectedElement[0], cc) : null, start, end, cc);
}
@Override
public final void run() {
try {
JavaSource source = JavaSource.forDocument(textC.getDocument());
source.runUserActionTask(this, true);
} catch (IOException ioe) {
ErrorManager.getDefault().notify(ioe);
return ;
}
TopComponent activetc = TopComponent.getRegistry().getActivated();
SHOW.show(ui, activetc);
}
protected abstract RefactoringUI createRefactoringUI(TreePathHandle selectedElement,int startOffset,int endOffset, CompilationInfo info);
}
private static final Set<TypeKind> NOT_ACCEPTED_TYPES = EnumSet.of(TypeKind.ERROR, TypeKind.NONE, TypeKind.OTHER, TypeKind.VOID);
static TreePath validateSelection(CompilationInfo ci, int start, int end) {
return validateSelection(ci, start, end, NOT_ACCEPTED_TYPES);
}
public static TreePath validateSelection(CompilationInfo ci, int start, int end, Set<TypeKind> ignoredTypes) {
if(start == end) {
TokenSequence<JavaTokenId> cts = ci.getTokenHierarchy().tokenSequence(JavaTokenId.language());
if (cts != null) {
cts.move(start);
if (cts.moveNext() && cts.token().id() != JavaTokenId.WHITESPACE && cts.offset() == start) {
start = end += 1;
}
}
}
TreePath tp = ci.getTreeUtilities().pathFor(start == end? start : (start + end) / 2 + 1);
for ( ; tp != null; tp = tp.getParentPath()) {
Tree leaf = tp.getLeaf();
if ( !ExpressionTree.class.isAssignableFrom(leaf.getKind().asInterface())
&& (leaf.getKind() != Tree.Kind.VARIABLE || ((VariableTree) leaf).getInitializer() == null)) {
continue;
}
long treeStart = ci.getTrees().getSourcePositions().getStartPosition(ci.getCompilationUnit(), leaf);
long treeEnd = ci.getTrees().getSourcePositions().getEndPosition(ci.getCompilationUnit(), leaf);
if (start != end) {
if (treeStart != start || treeEnd != end) {
continue;
}
} else {
if (treeStart != start && treeEnd != end) {
continue;
}
}
TypeMirror type = ci.getTrees().getTypeMirror(tp);
if (type != null && type.getKind() == TypeKind.ERROR) {
type = ci.getTrees().getOriginalType((ErrorType) type);
}
if (type == null || ignoredTypes.contains(type.getKind()))
continue;
if(tp.getLeaf().getKind() == Tree.Kind.ASSIGNMENT)
continue;
if (tp.getLeaf().getKind() == Tree.Kind.ANNOTATION)
continue;
if (!isInsideClass(tp))
return null;
TreePath candidate = tp;
tp = tp.getParentPath();
while (tp != null) {
switch (tp.getLeaf().getKind()) {
case VARIABLE:
VariableTree vt = (VariableTree) tp.getLeaf();
if (vt.getInitializer() == leaf) {
return candidate;
} else {
return null;
}
case NEW_CLASS:
NewClassTree nct = (NewClassTree) tp.getLeaf();
if (nct.getIdentifier().equals(candidate.getLeaf())) { //avoid disabling hint ie inside of anonymous class higher in treepath
for (Tree p : nct.getArguments()) {
if (p == leaf) {
return candidate;
}
}
return null;
}
}
leaf = tp.getLeaf();
tp = tp.getParentPath();
}
return candidate;
}
return null;
}
private static boolean isInsideClass(TreePath tp) {
while (tp != null) {
if (TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind()))
return true;
tp = tp.getParentPath();
}
return false;
}
private static abstract class NodeToElementTask implements Runnable, CancellableTask<CompilationController> {
private Node node;
private RefactoringUI ui;
public NodeToElementTask(Collection<? extends Node> nodes) {
assert nodes.size() == 1;
this.node = nodes.iterator().next();
}
@Override
public void cancel() {
}
@Override
public void run(CompilationController info) throws Exception {
info.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
CompilationUnitTree unit = info.getCompilationUnit();
if (unit.getTypeDecls().isEmpty()) {
ui = createRefactoringUI(null, info);
} else {
TreePathHandle representedObject = TreePathHandle.create(TreePath.getPath(unit, unit.getTypeDecls().get(0)),info);
ui = createRefactoringUI(representedObject, info);
}
}
@Override
public final void run() {
DataObject o = node.getCookie(DataObject.class);
JavaSource source = JavaSource.forFileObject(o.getPrimaryFile());
assert source != null;
try {
source.runUserActionTask(this, true);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
if (ui!=null) {
UI.openRefactoringUI(ui);
} else {
JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_NoTypeDecls"));
}
}
protected abstract RefactoringUI createRefactoringUI(TreePathHandle selectedElement, CompilationInfo info);
}
private static abstract class NodeToFileObjectTask implements Runnable, CancellableTask<CompilationController> {
private Collection<? extends Node> nodes;
public ArrayList<NonRecursiveFolder> pkg;
Collection<TreePathHandle> handles = new ArrayList<TreePathHandle>();
private Node currentNode;
public NodeToFileObjectTask(Collection<? extends Node> nodes) {
this.nodes = nodes;
}
protected int size() {
return nodes.size();
}
@Override
public void cancel() {
}
@Override
public void run(CompilationController info) throws Exception {
info.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
Collection<TreePathHandle> handlesPerNode = new ArrayList<TreePathHandle>();
CompilationUnitTree unit = info.getCompilationUnit();
Collection<TreePathHandle> publicHandles = new ArrayList<TreePathHandle>();
Collection<TreePathHandle> sameNameHandles = new ArrayList<TreePathHandle>();
for (Tree t: unit.getTypeDecls()) {
Element e = info.getTrees().getElement(TreePath.getPath(unit, t));
if (e == null || !(e.getKind().isClass() || e.getKind().isInterface())) {
// syntax errors #111195
continue;
}
if (e.getSimpleName().toString().equals(info.getFileObject().getName())) {
TreePathHandle representedObject = TreePathHandle.create(TreePath.getPath(unit,t),info);
sameNameHandles.add(representedObject);
}
if (e.getModifiers().contains(Modifier.PUBLIC)) {
TreePathHandle representedObject = TreePathHandle.create(TreePath.getPath(unit,t),info);
publicHandles.add(representedObject);
}
}
if (!publicHandles.isEmpty()) {
handlesPerNode.addAll(publicHandles);
} else {
handlesPerNode.addAll(sameNameHandles);
}
if (!handlesPerNode.isEmpty()) {
handles.addAll(handlesPerNode);
nodeTranslated(currentNode, handlesPerNode, info);
}
}
@Override
public void run() {
FileObject[] fobs = new FileObject[nodes.size()];
pkg = new ArrayList<NonRecursiveFolder>();
int i = 0;
for (Node node:nodes) {
DataObject dob = node.getCookie(DataObject.class);
if (dob!=null) {
fobs[i] = dob.getPrimaryFile();
if (RefactoringUtils.isJavaFile(fobs[i])) {
JavaSource source = JavaSource.forFileObject(fobs[i]);
assert source != null;
try {
currentNode = node;
// XXX this could be optimize by ClasspasthInfo in case of more than one file
source.runUserActionTask(this, true);
} catch (IllegalArgumentException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
} finally {
currentNode = null;
}
}
final NonRecursiveFolder nonrecursivefolder = node.getLookup().lookup(NonRecursiveFolder.class);
if (nonrecursivefolder !=null) {
pkg.add(nonrecursivefolder);
}
i++;
}
}
RefactoringUI ui = createRefactoringUI(fobs, handles);
if (ui!=null) {
UI.openRefactoringUI(ui);
} else {
JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_NoTypeDecls"));
}
}
/**
* Notifies subclasses about the translation.
* This is the place where subclasses may collect info about handles.
* @param node node that is translated
* @param handles handles translated from the node
* @param javac context of running translation
*/
protected void nodeTranslated(Node node, Collection<TreePathHandle> handles, CompilationInfo javac) {
}
protected abstract RefactoringUI createRefactoringUI(FileObject[] selectedElement, Collection<TreePathHandle> handles);
}
private static boolean nodeHandle(Lookup lookup) {
Node n = lookup.lookup(Node.class);
if (n!=null) {
if (n.getLookup().lookup(TreePathHandle.class)!=null) {
return true;
}
}
return false;
}
public static ShowUI SHOW = new ShowUI() {
@Override
public void show(RefactoringUI ui, TopComponent activetc) {
if (ui!=null) {
UI.openRefactoringUI(ui, activetc);
} else {
JOptionPane.showMessageDialog(null,NbBundle.getMessage(RefactoringActionsProvider.class, "ERR_CannotRenameKeyword"));
}
}
};
public interface ShowUI {
public void show(RefactoringUI ui, TopComponent activetc);
}
}