| /* |
| * 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("<"); |
| |
| for (TypeParameterElement e : list) { |
| if (addSpace) { |
| result.append(", "); |
| } |
| |
| result.append(getTypeName(info, e.asType(), true)); |
| |
| addSpace = true; |
| } |
| |
| result.append(">"); |
| } |
| |
| private void dumpRealTypeArguments(List<? extends TypeMirror> list) { |
| if (list.isEmpty()) |
| return ; |
| |
| boolean addSpace = false; |
| |
| result.append("<"); |
| |
| for (TypeMirror t : list) { |
| if (addSpace) { |
| result.append(", "); |
| } |
| |
| result.append(getTypeName(info, t, true)); |
| |
| addSpace = true; |
| } |
| |
| result.append(">"); |
| } |
| |
| 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[] {"&", "<", ">", "<br>", """}; // NOI18N |
| |
| private static String translate(String input) { |
| for (int cntr = 0; cntr < c.length; cntr++) { |
| input = input.replaceAll(c[cntr], tags[cntr]); |
| } |
| |
| return input; |
| } |
| |
| } |