blob: ad760d7e6ff6e931ca50852b75e1058d14377ce6 [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.web.el.navigation;
import com.sun.el.parser.AstDotSuffix;
import com.sun.el.parser.AstIdentifier;
import com.sun.el.parser.AstMethodArguments;
import com.sun.el.parser.AstString;
import com.sun.el.parser.Node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.AbstractElementVisitor6;
import javax.swing.text.Document;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.ui.ElementOpen;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.progress.ProgressUtils;
import org.netbeans.lib.editor.hyperlink.spi.HyperlinkProviderExt;
import org.netbeans.lib.editor.hyperlink.spi.HyperlinkType;
import org.netbeans.modules.csl.api.DataLoadersBridge;
import org.netbeans.modules.el.lexer.api.ELTokenId;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.netbeans.modules.web.common.api.WebUtils;
import org.netbeans.modules.web.el.CompilationContext;
import org.netbeans.modules.web.el.ELElement;
import org.netbeans.modules.web.el.ELLanguage;
import org.netbeans.modules.web.el.ELParserResult;
import org.netbeans.modules.web.el.ELTypeUtilities;
import org.netbeans.modules.web.el.ResourceBundles;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
/**
* Go to declaration for Expression Language.
*
* @author Erno Mononen
*/
public final class ELHyperlinkProvider implements HyperlinkProviderExt {
@Override
public boolean isHyperlinkPoint(final Document doc, final int offset, HyperlinkType type) {
final AtomicBoolean ret = new AtomicBoolean(false);
doc.render(new Runnable() {
@Override
public void run() {
ret.set(getELIdentifierSpan(doc, offset) != null);
}
});
return ret.get();
}
@Override
public int[] getHyperlinkSpan(final Document doc, final int offset, HyperlinkType type) {
final AtomicReference<int[]> ret = new AtomicReference<>();
doc.render(new Runnable() {
@Override
public void run() {
ret.set(getELIdentifierSpan(doc, offset));
}
});
return ret.get();
}
@Override
public void performClickAction(final Document doc, final int offset, HyperlinkType type) {
final AtomicBoolean cancel = new AtomicBoolean();
ProgressUtils.runOffEventDispatchThread(new Runnable() {
@Override
public void run() {
performGoTo(doc, offset, cancel);
}
}, NbBundle.getMessage(ELHyperlinkProvider.class, "LBL_GoToDeclaration"), cancel, false);
}
@Override
public Set<HyperlinkType> getSupportedHyperlinkTypes() {
return EnumSet.of(HyperlinkType.GO_TO_DECLARATION);
}
@Override
public String getTooltipText(Document doc, int offset, HyperlinkType type) {
Pair<Node, ELElement> nodeAndElement = resolveNodeAndElement(doc, offset, new AtomicBoolean());
if (nodeAndElement != null) {
if (nodeAndElement.first() instanceof AstString) {
// could be a resource bundle key
return getTooltipTextForBundleKey(nodeAndElement);
} else {
return getTooltipTextForElement(nodeAndElement);
}
}
return null;
}
private String getTooltipTextForElement(final Pair<Node, ELElement> pair) {
final String[] result = new String[1];
final FileObject file = pair.second().getSnapshot().getSource().getFileObject();
ClasspathInfo cp = ELTypeUtilities.getElimplExtendedCPI(file);
try {
JavaSource.create(cp, file).runUserActionTask(new Task<CompilationController>() {
@Override
public void run(CompilationController cc) throws Exception {
cc.toPhase(JavaSource.Phase.RESOLVED);
final Element resolvedElement = ELTypeUtilities.resolveElement(CompilationContext.create(file, cc), pair.second(), pair.first());
if (resolvedElement == null) {
return;
}
DisplayNameElementVisitor dnev = new DisplayNameElementVisitor(cc);
dnev.visit(resolvedElement, Boolean.TRUE);
result[0] = "<html><body>" + dnev.result.toString(); //NOI18N
}
}, true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return result[0];
}
private String getTooltipTextForBundleKey(Pair<Node, ELElement> pair) {
FileObject context = pair.second().getSnapshot().getSource().getFileObject();
ResourceBundles resourceBundles = ResourceBundles.get(context);
if (!resourceBundles.canHaveBundles()) {
return null;
}
for (Pair<AstIdentifier, Node> each : resourceBundles.collectKeys(pair.second().getNode())) {
if (each.second().equals(pair.first())) {
StringBuilder result = new StringBuilder();
String key = each.second().getImage();
String value = resourceBundles.getValue(each.first().getImage(), each.second().getImage());
result.append("<html><body>")
.append(key)
.append("=<font color='#ce7b00'>") // NOI18N
.append(value)
.append("</font>"); // NOI18N
return result.toString();
}
}
return null;
}
protected static Pair<Node, ELElement> resolveNodeAndElement(final Document doc, final int offset, final AtomicBoolean cancel) {
Source source = Source.create(doc);
return resolveNodeAndElement(source, offset, cancel);
}
/**
* Resolves the Node at the given offset.
* @return the node and the ELElement containing it or null.
*/
protected static Pair<Node, ELElement> resolveNodeAndElement(final Source source, final int offset, final AtomicBoolean cancel) {
final List<Pair<Node,ELElement>> result = new ArrayList<>(1);
try {
ParserManager.parse(Collections.singletonList(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
ResultIterator elRi = WebUtils.getResultIterator(resultIterator, ELLanguage.MIME_TYPE);
if (cancel.get()) {
return;
}
ELParserResult parserResult = (ELParserResult) elRi.getParserResult();
ELElement elElement = parserResult.getElementAt(offset);
if (elElement == null || !elElement.isValid()) {
return;
}
Node node = elElement.findNodeAt(offset);
if (node == null) {
return;
}
if(node instanceof AstMethodArguments) {
node = node.jjtGetParent();
assert node instanceof AstDotSuffix;
}
result.add(Pair.<Node, ELElement>of(node, elElement));
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
return result.isEmpty() ? null : result.get(0);
}
private void performGoTo(final Document doc, final int offset, final AtomicBoolean cancel) {
final Pair<Node, ELElement> nodeElem = resolveNodeAndElement(doc, offset, cancel);
if (nodeElem == null) {
return;
}
final FileObject file = DataLoadersBridge.getDefault().getFileObject(doc);
ClasspathInfo cp = ELTypeUtilities.getElimplExtendedCPI(file);
final AtomicReference<ElementHandle<Element>> handleRef = new AtomicReference<>();
try {
JavaSource.create(cp).runUserActionTask(new Task<CompilationController>() {
@Override
public void run(CompilationController cc) throws Exception {
cc.toPhase(JavaSource.Phase.RESOLVED);
Element javaElement = ELTypeUtilities.resolveElement(CompilationContext.create(file, cc), nodeElem.second(), nodeElem.first());
if(javaElement != null) {
handleRef.set(ElementHandle.create(javaElement));
}
}
}, true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
ElementHandle<Element> handle = handleRef.get();
if(handle != null) {
ElementOpen.open(cp, handle);
}
}
protected static int[] getELIdentifierSpan(Document doc, int offset) {
TokenSequence<?> elTokenSequence = LexerUtils.getTokenSequence(doc, offset, ELTokenId.language(), false);
if (elTokenSequence == null) {
return null;
}
elTokenSequence.move(offset);
if (!elTokenSequence.moveNext()) {
return null; //no token
}
if (elTokenSequence.token().id() == ELTokenId.IDENTIFIER
|| elTokenSequence.token().id() == ELTokenId.STRING_LITERAL) { // string for bundle keys
return new int[]{elTokenSequence.offset(), elTokenSequence.offset() + elTokenSequence.token().length()};
}
return null;
}
/**
* This is a copy of {@code org.netbeans.modules.editor.java.GoToSupport.DisplayNameElementVisitor}.
* See #189669.
*/
private static final class DisplayNameElementVisitor extends AbstractElementVisitor6<Void, Boolean> {
private final CompilationInfo info;
public DisplayNameElementVisitor(CompilationInfo info) {
this.info = info;
}
private StringBuffer result = new StringBuffer();
private void boldStartCheck(boolean highlightName) {
if (highlightName) {
result.append("<b>");
}
}
private void boldStopCheck(boolean highlightName) {
if (highlightName) {
result.append("</b>");
}
}
@Override
public Void visitPackage(PackageElement e, Boolean highlightName) {
boldStartCheck(highlightName);
result.append(e.getQualifiedName());
boldStopCheck(highlightName);
return null;
}
@Override
public Void visitType(TypeElement e, Boolean highlightName) {
return printType(e, null, highlightName);
}
Void printType(TypeElement e, DeclaredType dt, Boolean highlightName) {
modifier(e.getModifiers());
switch (e.getKind()) {
case CLASS:
result.append("class ");
break;
case INTERFACE:
result.append("interface ");
break;
case ENUM:
result.append("enum ");
break;
case ANNOTATION_TYPE:
result.append("@interface ");
break;
default:
break;
}
Element enclosing = e.getEnclosingElement();
if (enclosing == info.getElementUtilities().enclosingTypeElement(e)) {
result.append(((TypeElement) enclosing).getQualifiedName());
result.append('.');
boldStartCheck(highlightName);
result.append(e.getSimpleName());
boldStopCheck(highlightName);
} else {
result.append(e.getQualifiedName());
}
if (dt != null)
dumpRealTypeArguments(dt.getTypeArguments());
return null;
}
@Override
public Void visitVariable(VariableElement e, Boolean highlightName) {
modifier(e.getModifiers());
result.append(getTypeName(info, e.asType(), true));
result.append(' ');
boldStartCheck(highlightName);
result.append(e.getSimpleName());
boldStopCheck(highlightName);
if (highlightName) {
if (e.getConstantValue() != null) {
result.append(" = ");
result.append(e.getConstantValue().toString());
}
Element enclosing = e.getEnclosingElement();
if (e.getKind() != ElementKind.PARAMETER && e.getKind() != ElementKind.LOCAL_VARIABLE && e.getKind() != ElementKind.EXCEPTION_PARAMETER) {
result.append(" in ");
//short typename:
result.append(getTypeName(info, enclosing.asType(), true));
}
}
return null;
}
@Override
public Void visitExecutable(ExecutableElement e, Boolean highlightName) {
return printExecutable(e, null, highlightName);
}
Void printExecutable(ExecutableElement e, DeclaredType dt, Boolean highlightName) {
switch (e.getKind()) {
case CONSTRUCTOR:
modifier(e.getModifiers());
dumpTypeArguments(e.getTypeParameters());
result.append(' ');
boldStartCheck(highlightName);
result.append(e.getEnclosingElement().getSimpleName());
boldStopCheck(highlightName);
if (dt != null) {
dumpRealTypeArguments(dt.getTypeArguments());
dumpArguments(e.getParameters(), ((ExecutableType) info.getTypes().asMemberOf(dt, e)).getParameterTypes());
} else {
dumpArguments(e.getParameters(), null);
}
dumpThrows(e.getThrownTypes());
break;
case METHOD:
modifier(e.getModifiers());
dumpTypeArguments(e.getTypeParameters());
result.append(getTypeName(info, e.getReturnType(), true));
result.append(' ');
boldStartCheck(highlightName);
result.append(e.getSimpleName());
boldStopCheck(highlightName);
dumpArguments(e.getParameters(), null);
dumpThrows(e.getThrownTypes());
break;
case INSTANCE_INIT:
case STATIC_INIT:
//these two cannot be referenced anyway...
}
return null;
}
@Override
public Void visitTypeParameter(TypeParameterElement e, Boolean highlightName) {
return null;
}
private void modifier(Set<Modifier> modifiers) {
boolean addSpace = false;
for (Modifier m : modifiers) {
if (addSpace) {
result.append(' ');
}
addSpace = true;
result.append(m.toString());
}
if (addSpace) {
result.append(' ');
}
}
private void dumpTypeArguments(List<? extends TypeParameterElement> list) {
if (list.isEmpty())
return ;
boolean addSpace = false;
result.append("&lt;");
for (TypeParameterElement e : list) {
if (addSpace) {
result.append(", ");
}
result.append(getTypeName(info, e.asType(), true));
addSpace = true;
}
result.append("&gt;");
}
private void dumpRealTypeArguments(List<? extends TypeMirror> list) {
if (list.isEmpty())
return ;
boolean addSpace = false;
result.append("&lt;");
for (TypeMirror t : list) {
if (addSpace) {
result.append(", ");
}
result.append(getTypeName(info, t, true));
addSpace = true;
}
result.append("&gt;");
}
private void dumpArguments(List<? extends VariableElement> list, List<? extends TypeMirror> types) {
boolean addSpace = false;
result.append('(');
Iterator<? extends VariableElement> listIt = list.iterator();
Iterator<? extends TypeMirror> typesIt = types != null ? types.iterator() : null;
while (listIt.hasNext()) {
if (addSpace) {
result.append(", ");
}
VariableElement ve = listIt.next();
TypeMirror type = typesIt != null ? typesIt.next() : ve.asType();
result.append(getTypeName(info, type, true));
result.append(" ");
result.append(ve.getSimpleName());
addSpace = true;
}
result.append(')');
}
private void dumpThrows(List<? extends TypeMirror> list) {
if (list.isEmpty())
return ;
boolean addSpace = false;
result.append(" throws ");
for (TypeMirror t : list) {
if (addSpace) {
result.append(", ");
}
result.append(getTypeName(info, t, true));
addSpace = true;
}
}
}
private static String getTypeName(CompilationInfo info, TypeMirror t, boolean fqn) {
return translate(info.getTypeUtilities().getTypeName(t).toString());
}
private static String[] c = new String[] {"&", "<", ">", "\n", "\""}; // NOI18N
private static String[] tags = new String[] {"&amp;", "&lt;", "&gt;", "<br>", "&quot;"}; // NOI18N
private static String translate(String input) {
for (int cntr = 0; cntr < c.length; cntr++) {
input = input.replaceAll(c[cntr], tags[cntr]);
}
return input;
}
}