blob: 7800ff96220e781070b38508327a41c424159e8f [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.java.navigation;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ExportsTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModuleTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.OpensTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ProvidesTree;
import com.sun.source.tree.RequiresTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.UsesTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import org.netbeans.api.java.source.support.ErrorAwareTreeScanner;
import com.sun.source.util.Trees;
import java.awt.Image;
import java.io.CharConversionException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.ModuleElement;
import javax.swing.Icon;
import org.netbeans.api.actions.Openable;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.java.source.UiUtils;
import org.netbeans.api.java.source.ui.ElementIcons;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.editor.breadcrumbs.spi.BreadcrumbsController;
import org.netbeans.modules.editor.breadcrumbs.spi.BreadcrumbsElement;
import org.openide.cookies.OpenCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbCollections;
import org.openide.util.lookup.Lookups;
import org.openide.xml.XMLUtil;
/**
*
* @author Dusan Balek
*/
public class BreadCrumbsNodeImpl implements BreadcrumbsElement {
private static final String COLOR = "#707070";
private final Lookup lookup;
private final BreadCrumbsNodeImpl parent;
private final TreePathHandle tph;
private final Callable<? extends Image> iconProvider;
private final String htmlDisplayName;
// private OpenAction openAction;
public BreadCrumbsNodeImpl(final BreadCrumbsNodeImpl parent, final TreePathHandle tph,
final Image icon, final String htmlDisplayName, final FileObject fileObject, final int[] pos) {
this(
parent,
tph,
new Callable<Image> () {
@Override
@CheckForNull
public Image call() throws Exception {
return icon;
}
},
htmlDisplayName,
fileObject,
pos);
}
private BreadCrumbsNodeImpl(
@NullAllowed final BreadCrumbsNodeImpl parent,
@NonNull final TreePathHandle tph,
@NonNull final Callable<? extends Image> iconProvider,
@NullAllowed final String htmlDisplayName,
@NonNull final FileObject fileObject,
@NonNull final int[] pos) {
this.lookup = Lookups.fixed(tph, pos, new OpenableImpl(fileObject, pos[0]));
this.parent = parent;
this.tph = tph;
this.iconProvider = iconProvider;
this.htmlDisplayName = htmlDisplayName;
}
@Override
public String getHtmlDisplayName() {
return htmlDisplayName;
}
@Override
public Image getIcon(int type) {
try {
return iconProvider.call();
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
return null;
}
}
@Override
public Image getOpenedIcon(int type) {
return getIcon(type);
}
private static final String CONSTRUCTOR_NAME = "<init>";
private static final String ERR_NAME = "<error>";
public static BreadCrumbsNodeImpl createBreadcrumbs(BreadCrumbsNodeImpl parent, final CompilationInfo info, TreePath path, boolean elseSection) {
final Trees trees = info.getTrees();
final SourcePositions sp = trees.getSourcePositions();
int[] pos = new int[] {(int) sp.getStartPosition(path.getCompilationUnit(), path.getLeaf()), (int) sp.getEndPosition(path.getCompilationUnit(), path.getLeaf())};
final Tree leaf = path.getLeaf();
switch (leaf.getKind()) {
case COMPILATION_UNIT:
TreePathHandle tph = TreePathHandle.create(path, info);
return new BreadCrumbsNodeImpl(parent, tph, (Image) null, FileUtil.getFileDisplayName(info.getFileObject()), info.getFileObject(), pos);
case MODULE:
tph = TreePathHandle.create(path, info);
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), ((ModuleTree)leaf).getName().toString(), info.getFileObject(), pos);
case REQUIRES:
tph = TreePathHandle.create(path, info);
StringBuilder sb = new StringBuilder("requires "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((RequiresTree) leaf).getModuleName().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), sb.toString(), info.getFileObject(), pos);
case EXPORTS:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("exports "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((ExportsTree) leaf).getPackageName().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), sb.toString(), info.getFileObject(), pos);
case OPENS:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("opens "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((OpensTree) leaf).getPackageName().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), sb.toString(), info.getFileObject(), pos);
case PROVIDES:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("provides "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(simpleName(((ProvidesTree) leaf).getServiceName()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), sb.toString(), info.getFileObject(), pos);
case USES:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("uses "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(simpleName(((UsesTree) leaf).getServiceName()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), sb.toString(), info.getFileObject(), pos);
case CLASS:
case INTERFACE:
case ENUM:
case ANNOTATION_TYPE:
tph = TreePathHandle.create(path, info);
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), className(path), info.getFileObject(), pos);
case METHOD:
tph = TreePathHandle.create(path, info);
MethodTree mt = (MethodTree) leaf;
CharSequence name;
if (mt.getName().contentEquals(CONSTRUCTOR_NAME)) {
name = ((ClassTree) path.getParentPath().getLeaf()).getSimpleName();
} else {
name = mt.getName();
}
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), name.toString(), info.getFileObject(), pos);
case VARIABLE:
tph = TreePathHandle.create(path, info);
return new BreadCrumbsNodeImpl(parent, tph, iconProviderFor(info, path), ((VariableTree) leaf).getName().toString(), info.getFileObject(), pos);
case CASE:
tph = TreePathHandle.create(path, info);
ExpressionTree expr = ((CaseTree) leaf).getExpression();
sb = new StringBuilder(expr == null ? "default:" : "case "); //NOI18N
if (expr != null) {
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((CaseTree) leaf).getExpression().toString()));
sb.append(":"); //NOI18N
sb.append("</font>"); //NOI18N
}
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case CATCH:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("catch "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((CatchTree) leaf).getParameter().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case DO_WHILE_LOOP:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("do ... while "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((DoWhileLoopTree) leaf).getCondition().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case ENHANCED_FOR_LOOP:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("for "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append("("); //NOI18N
sb.append(escape(((EnhancedForLoopTree) leaf).getVariable().toString()));
sb.append(" : "); //NOI18N
sb.append(escape(((EnhancedForLoopTree) leaf).getExpression().toString()));
sb.append(")"); //NOI18N
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case FOR_LOOP:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("for "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append("("); //NOI18N
List<? extends StatementTree> initializer = ((ForLoopTree) leaf).getInitializer();
if (!initializer.isEmpty() && initializer.get(0).getKind() == Kind.VARIABLE) {
sb.append(escape(initializer.get(0).toString()));
for (VariableTree currentVar : NbCollections.checkedListByCopy(initializer.subList(1, initializer.size()), VariableTree.class, true)) {
sb.append(", ");
sb.append(escape(currentVar.getName().toString()));
if (currentVar.getInitializer() != null) {
sb.append(" = ");
sb.append(escape(currentVar.getInitializer().toString()));
}
}
} else {
boolean first = true;
for (StatementTree init : initializer) {
if (!first) sb.append(", ");
if (init.getKind() == Kind.EXPRESSION_STATEMENT) {
sb.append(((ExpressionStatementTree) init).getExpression().toString());
} else {
sb.append(init.toString());
}
first = false;
}
}
sb.append("; "); //NOI18N
if (((ForLoopTree) leaf).getCondition() != null) {
sb.append(escape(((ForLoopTree) leaf).getCondition().toString()));
}
sb.append("; "); //NOI18N
boolean first = true;
for (ExpressionStatementTree update : ((ForLoopTree) leaf).getUpdate()) {
if (!first) sb.append(", ");
sb.append(update.getExpression().toString());
first = false;
}
sb.append(")"); //NOI18N
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case IF:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder(""); //NOI18N
Tree last = leaf;
while (path != null && path.getLeaf().getKind() == Kind.IF) {
StringBuilder temp = new StringBuilder(""); //NOI18N
temp.append("if "); //NOI18N
temp.append("<font color=").append(COLOR).append(">"); // NOI18N
temp.append(escape(((IfTree) path.getLeaf()).getCondition().toString()));
temp.append("</font>"); //NOI18N
if (((IfTree) path.getLeaf()).getElseStatement() == last || (path.getLeaf() == leaf && elseSection)) {
temp.append(" else");
}
temp.append(" ");
sb.insert(0, temp.toString());
last = path.getLeaf();
path = path.getParentPath();
}
sb.delete(sb.length() - 1, sb.length());
IfTree it = (IfTree) leaf;
int elseStart = pos[1] + 1;
if (it.getElseStatement() != null) {
TokenSequence<JavaTokenId> ts = info.getTokenHierarchy().tokenSequence(JavaTokenId.language());
ts.move(elseStart = (int) sp.getStartPosition(path.getCompilationUnit(), it.getElseStatement()));
boolean success;
while ((success = ts.movePrevious()) && ts.token().id() != JavaTokenId.ELSE)
;
elseStart = success ? Math.min(ts.offset(), elseStart) : elseStart;
}
if (elseSection) {
int endPos = (int) sp.getEndPosition(path.getCompilationUnit(), it.getElseStatement());
if (it.getElseStatement().getKind() == Kind.IF) {
endPos = (int) sp.getStartPosition(path.getCompilationUnit(), it.getElseStatement()) - 1;
}
pos = new int[] {elseStart, endPos};
} else {
pos[1] = elseStart - 1;
}
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case SWITCH:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("switch "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((SwitchTree) leaf).getExpression().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case SYNCHRONIZED:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("synchronized "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((SynchronizedTree) leaf).getExpression().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case TRY:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("try"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case WHILE_LOOP:
tph = TreePathHandle.create(path, info);
sb = new StringBuilder("while "); //NOI18N
sb.append("<font color=").append(COLOR).append(">"); // NOI18N
sb.append(escape(((WhileLoopTree) leaf).getCondition().toString()));
sb.append("</font>"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
case BLOCK:
tph = TreePathHandle.create(path, info);
if (TreeUtilities.CLASS_TREE_KINDS.contains(path.getParentPath().getLeaf().getKind())) {
sb = new StringBuilder(((BlockTree)leaf).isStatic() ? "&lt;static init&gt;" : "&lt;init&gt;"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
} else if (path.getParentPath().getLeaf().getKind() == Kind.TRY && ((TryTree) path.getParentPath().getLeaf()).getFinallyBlock() == leaf) {
sb = new StringBuilder("finally"); //NOI18N
return new BreadCrumbsNodeImpl(parent, tph, DEFAULT_ICON, sb.toString(), info.getFileObject(), pos);
}
break;
}
return null;
}
private static final Pattern UNICODE_SEQUENCE = Pattern.compile("\\\\u([0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z])");
static String escape(String s) {
if (s != null) {
//unescape unicode sequences first (would be better if Pretty would not print them, but that might be more difficult):
Matcher matcher = UNICODE_SEQUENCE.matcher(s);
if (matcher.find()) {
StringBuilder result = new StringBuilder();
int lastReplaceEnd = 0;
do {
result.append(s.substring(lastReplaceEnd, matcher.start()));
int ch = Integer.parseInt(matcher.group(1), 16);
result.append((char) ch);
lastReplaceEnd = matcher.end();
} while (matcher.find());
result.append(s.substring(lastReplaceEnd));
s = result.toString();
}
try {
return XMLUtil.toAttributeValue(s);
} catch (CharConversionException ex) {
}
}
return null;
}
private static final Image DEFAULT_ICON = BreadcrumbsController.NO_ICON;
private static Callable<? extends Image> iconProviderFor(CompilationInfo info, TreePath path) {
Element el = info.getTrees().getElement(path);
final ElementKind kind = el == null ? null : el.getKind();
final Set<Modifier> modifiers = el == null ? null : el.getModifiers();
final ModuleElement.DirectiveKind directiveKind = directiveKind(path.getLeaf().getKind());
return new Callable<Image>() {
@Override
public Image call() throws Exception {
Icon icon = null;
if (kind == null) {
if (directiveKind == null)
return DEFAULT_ICON;
icon = ElementIcons.getModuleDirectiveIcon(directiveKind);
} else {
assert modifiers != null;
icon = ElementIcons.getElementIcon(kind, modifiers);
}
if (icon == null) return DEFAULT_ICON;
return ImageUtilities.icon2Image(icon);
}
};
}
private static ModuleElement.DirectiveKind directiveKind(Kind treeKind) {
switch (treeKind) {
case EXPORTS:
return ModuleElement.DirectiveKind.EXPORTS;
case PROVIDES:
return ModuleElement.DirectiveKind.PROVIDES;
case REQUIRES:
return ModuleElement.DirectiveKind.REQUIRES;
case USES:
return ModuleElement.DirectiveKind.USES;
}
return null;
}
private static String className(TreePath path) {
ClassTree ct = (ClassTree) path.getLeaf();
if (path.getParentPath().getLeaf().getKind() == Kind.NEW_CLASS) {
NewClassTree nct = (NewClassTree) path.getParentPath().getLeaf();
if (nct.getClassBody() == ct) {
return simpleName(nct.getIdentifier());
}
} else if (path.getParentPath().getLeaf() == path.getCompilationUnit()) {
ExpressionTree pkg = path.getCompilationUnit().getPackageName();
String pkgName = pkg != null ? pkg.toString() : null;
if (pkgName != null && !pkgName.contentEquals(ERR_NAME)) {
return pkgName + '.' + ct.getSimpleName().toString();
}
}
return ct.getSimpleName().toString();
}
private static String simpleName(Tree t) {
switch (t.getKind()) {
case PARAMETERIZED_TYPE:
return simpleName(((ParameterizedTypeTree) t).getType());
case IDENTIFIER:
return ((IdentifierTree) t).getName().toString();
case MEMBER_SELECT:
return ((MemberSelectTree) t).getIdentifier().toString();
default:
return "";//XXX
}
}
private final AtomicReference<List<BreadcrumbsElement>> children = new AtomicReference<>();
public List<BreadcrumbsElement> getChildren() {
List<BreadcrumbsElement> cached = children.get();
if (cached != null) return cached;
final List<BreadcrumbsElement> result = new ArrayList<>();
try {
final FileObject file = tph.getFileObject();
if (file == null) {
return result;
}
JavaSource js = JavaSource.forFileObject(file);
if (js == null) return result;
js.runUserActionTask(new Task<CompilationController>() {
@Override public void run(final CompilationController cc) throws Exception {
cc.toPhase(Phase.RESOLVED); //XXX: resolved?
final TreePath tp = tph.resolve(cc);
if (tp == null) {
//XXX: log
return;
}
tp.getLeaf().accept(new ErrorAwareTreeScanner<Void, TreePath>() {
@Override public Void scan(Tree node, TreePath p) {
if (node == null) return null;
if (node.getKind() == Kind.IF) {
IfTree it = (IfTree) node;
BreadCrumbsNodeImpl n = createBreadcrumbs(BreadCrumbsNodeImpl.this, cc, new TreePath(p, node), false);
assert n != null;
result.add(n);
if (it.getElseStatement() != null) {
n = createBreadcrumbs(BreadCrumbsNodeImpl.this, cc, new TreePath(p, node), true);
assert n != null;
result.add(n);
if (it.getElseStatement().getKind() == Kind.IF) {
scan((IfTree) it.getElseStatement(), new TreePath(p, node));
}
}
return null;
}
p = new TreePath(p, node);
if (cc.getTreeUtilities().isSynthetic(p)) return null;
BreadCrumbsNodeImpl n = createBreadcrumbs(BreadCrumbsNodeImpl.this, cc, p, false);
if (n != null) {
result.add(n);
} else {
return super.scan(node, p);
}
return null;
}
@Override public Void visitMethod(MethodTree node, TreePath p) {
return scan(node.getBody(), p);
}
}, tp);
}
}, true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return children.compareAndSet(null, result) ? result : children.get();
}
public Lookup getLookup() {
return lookup;
}
@Override
public BreadcrumbsElement getParent() {
return parent;
}
private static final class OpenableImpl implements Openable, OpenCookie {
private final FileObject file;
private final int pos;
public OpenableImpl(FileObject file, int pos) {
this.file = file;
this.pos = pos;
}
@Override
public void open() {
UiUtils.open(file, pos);
}
}
}