blob: 5fb44e3581faa98872f2f6923f2843567b9db6bc [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.completion;
import com.sun.el.parser.AstDotSuffix;
import com.sun.el.parser.AstIdentifier;
import com.sun.el.parser.AstMethodArguments;
import com.sun.el.parser.Node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementScanner6;
import org.netbeans.api.java.source.ClassIndex;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.modules.csl.api.CodeCompletionContext;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.spi.DefaultCompletionProposal;
import org.netbeans.modules.web.el.AstPath;
import org.netbeans.modules.web.el.CompilationContext;
import org.netbeans.modules.web.el.ELElement;
import org.netbeans.modules.web.el.completion.ELCodeCompletionHandler.PrefixMatcher;
/**
* Provides java completion inside the EL expressions - for calls of static field, methods and constructors.
*
* @author Martin Fousek <marfous@netbeans.org>
*/
public class ELJavaCompletion {
private ELJavaCompletion() {
}
private static final Set<Modifier> PUBLIC_STATIC = EnumSet.<Modifier>of(Modifier.PUBLIC, Modifier.STATIC);
private static final Set<ElementKind> FIELD_METHOD = EnumSet.<ElementKind>of(ElementKind.FIELD, ElementKind.METHOD);
private static final String JAVA_LANG_PACKAGE = "java.lang"; //NOI18N
private static final String JAVA_LANG_PREFIX = JAVA_LANG_PACKAGE + "."; //NOI18N
private static final PackageEntry DEFAULT_PACKAGE = new PackageEntry(JAVA_LANG_PACKAGE, true);
protected static void propose(
CompilationContext ccontext,
CodeCompletionContext context,
ELElement element,
Node targetNode,
List<CompletionProposal> proposals) throws IOException {
String prefix = getPrefixForNode(context, element, targetNode);
PrefixMatcher prefixMatcher = PrefixMatcher.create(prefix, context);
doJavaCompletion(ccontext, prefixMatcher, context.getCaretOffset() - prefix.length(), proposals);
}
private static String getPrefixForNode(CodeCompletionContext context, ELElement element, Node targetNode) {
if (targetNode instanceof AstDotSuffix) {
AstPath path = new AstPath(element.getNode());
return extractPrefixFromPath(path.rootToNode(targetNode)) + "."; //NOI18N
} else if (targetNode instanceof AstMethodArguments) {
AstPath path = new AstPath(targetNode);
return extractPrefixFromPath(path.rootToLeaf()); //NOI18N
}
return context.getPrefix() != null ? context.getPrefix() : ""; //NOI18N
}
private static void doJavaCompletion(CompilationContext ccontext, PrefixMatcher pm, final int offset, final List<CompletionProposal> proposals) throws IOException {
CompilationController cc = (CompilationController) ccontext.info();
String packName = pm.getPrefix();
int dotIndex = pm.getPrefix().lastIndexOf('.'); // NOI18N
if (dotIndex != -1) {
packName = pm.getPrefix().substring(0, dotIndex);
}
// adds packages to the CC
addPackages(cc, pm.getPrefix(), offset, proposals);
Set<PackageEntry> packages = new HashSet<>();
if (dotIndex == -1) {
// java.lang package is imported by default
packages.add(DEFAULT_PACKAGE);
}
if (!packName.isEmpty()) {
packages.add(new PackageEntry(packName, false));
}
// adds types to the CC
addTypesFromPackages(cc, pm, packages, offset + dotIndex + 1, proposals);
// adds element type fields and methods
TypeElement typeElement = cc.getElements().getTypeElement(packName);
if (typeElement == null && dotIndex != -1) {
typeElement = cc.getElements().getTypeElement(JAVA_LANG_PREFIX + pm.getPrefix().substring(0, dotIndex));
}
if (typeElement != null) {
proposals.addAll(getFieldsAndMethods(typeElement, offset + dotIndex + 1));
}
}
// private static void addAllTypes(final CompilationContext ccontext, CompilationController controler, final List<CompletionProposal> proposals) {
// String EMPTY = ""; //NOI18N
// String prefix = ""; //NOI18N
// ClassIndex.NameKind kind = ClassIndex.NameKind.CASE_INSENSITIVE_PREFIX;
// Set<ElementHandle<TypeElement>> declaredTypes = controler.getClasspathInfo().getClassIndex().getDeclaredTypes(prefix != null ? prefix : EMPTY, kind, EnumSet.allOf(ClassIndex.SearchScope.class));
// for (ElementHandle<TypeElement> name : declaredTypes) {
// proposals.add(new JavaTypeCompletionItem(ccontext, name));
// }
// }
private static void addTypesFromPackages(CompilationController cc, PrefixMatcher pm, Set<PackageEntry> packages, int offset, List<CompletionProposal> proposals) {
for (PackageEntry packageEntry : packages) {
PackageElement pkgElem = cc.getElements().getPackageElement(packageEntry.packageName);
if (pkgElem == null) {
continue;
}
List<TypeElement> tes = new TypeScanner().scan(pkgElem);
for (TypeElement te : tes) {
if (packageEntry.imported) {
if (pm.matches(te.getSimpleName().toString())) {
JavaTypeCompletionItem item = new JavaTypeCompletionItem(te, offset);
proposals.add(item);
}
} else {
if (pm.matches(te.getQualifiedName().toString())) {
JavaTypeCompletionItem item = new JavaTypeCompletionItem(te, offset);
proposals.add(item);
}
}
}
}
}
private static Collection<DefaultCompletionProposal> getFieldsAndMethods(TypeElement typeElement, int i) {
Map<String, DefaultCompletionProposal> classProposals = new HashMap<>();
for (Element element : typeElement.getEnclosedElements()) {
if (element.getModifiers().containsAll(PUBLIC_STATIC) && FIELD_METHOD.contains(element.getKind())) {
classProposals.put(element.getSimpleName().toString(), new JavaFieldCompletionItem(element, typeElement.getSimpleName().toString(), i));
}
}
return classProposals.values();
}
private static void addPackages(CompilationController controler, String fqnPrefix, int offset, final List<CompletionProposal> proposals) {
if (fqnPrefix == null || fqnPrefix.isEmpty()) {
fqnPrefix = "java"; //NOI18N
}
for (String pkgName : controler.getClasspathInfo().getClassIndex().getPackageNames(
fqnPrefix, true, EnumSet.allOf(ClassIndex.SearchScope.class))) {
if (isImportedPackage(pkgName)) {
proposals.add(new JavaPackageCompletionItem(pkgName, offset));
}
}
}
private static String extractPrefixFromPath(List<Node> pathToNode) {
StringBuilder sb = new StringBuilder();
for (Node node : pathToNode) {
if (node instanceof AstIdentifier) {
sb.append(node.getImage());
} else if (node instanceof AstDotSuffix) {
sb.append(".").append(node.getImage()); //NOI18N
}
}
return sb.toString(); //NOI18N
}
private static boolean isImportedPackage(String pkgName) {
// TODO - get imported classes and allow them to be completed too. For now we show only classes
// enabled by default
return pkgName.startsWith("java.") || pkgName.startsWith("javax.")
|| pkgName.equals("java") || pkgName.equals("javax");
}
private static final class JavaTypeCompletionItem extends DefaultCompletionProposal {
private final String simpleName;
private final String fqn;
private final int offset;
public JavaTypeCompletionItem(TypeElement te, int offset) {
this.simpleName = te.getSimpleName().toString();
this.fqn = te.getQualifiedName().toString();
this.offset = offset;
}
@Override
public org.netbeans.modules.csl.api.ElementHandle getElement() {
return null;
}
@Override
public org.netbeans.modules.csl.api.ElementKind getKind() {
return org.netbeans.modules.csl.api.ElementKind.CLASS;
}
@Override
public String getName() {
return simpleName;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
return fqn;
}
@Override
public int getAnchorOffset() {
return offset;
}
}
private static final class JavaFieldCompletionItem extends DefaultCompletionProposal {
private final String name;
private final String enclosingName;
private final org.netbeans.modules.csl.api.ElementKind kind;
private final int offset;
public JavaFieldCompletionItem(Element e, String enclosingName, int offset) {
this.name = e.getSimpleName().toString();
this.enclosingName = enclosingName;
this.kind = e.getKind() == ElementKind.METHOD
? org.netbeans.modules.csl.api.ElementKind.METHOD
: org.netbeans.modules.csl.api.ElementKind.CONSTANT;
this.offset = offset;
}
@Override
public org.netbeans.modules.csl.api.ElementHandle getElement() {
return null;
}
@Override
public org.netbeans.modules.csl.api.ElementKind getKind() {
return kind;
}
@Override
public String getName() {
return name;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
return enclosingName;
}
@Override
public int getAnchorOffset() {
return offset;
}
@Override
public String getCustomInsertTemplate() {
if (isMethod()) {
return super.getCustomInsertTemplate() + "(${cursor})"; //NOI18N
}
return super.getCustomInsertTemplate();
}
private boolean isMethod() {
return kind == org.netbeans.modules.csl.api.ElementKind.METHOD;
}
}
private static final class JavaPackageCompletionItem extends DefaultCompletionProposal {
private final String name;
private final int offset;
public JavaPackageCompletionItem(String name, int offset) {
this.name = name;
this.offset = offset;
}
@Override
public org.netbeans.modules.csl.api.ElementHandle getElement() {
return null;
}
@Override
public org.netbeans.modules.csl.api.ElementKind getKind() {
return org.netbeans.modules.csl.api.ElementKind.PACKAGE;
}
@Override
public int getAnchorOffset() {
return offset;
}
@Override
public String getName() {
return name;
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
return name;
}
}
private static final class TypeScanner extends ElementScanner6<List<TypeElement>, Void> {
public TypeScanner() {
super(new ArrayList<TypeElement>());
}
private static boolean isAccessibleClass(TypeElement te) {
NestingKind nestingKind = te.getNestingKind();
return (nestingKind == NestingKind.TOP_LEVEL && te.getModifiers().contains(Modifier.PUBLIC));
}
@Override
public List<TypeElement> visitType(TypeElement typeElement, Void arg) {
if (typeElement.getKind() == ElementKind.CLASS && isAccessibleClass(typeElement)) {
DEFAULT_VALUE.add(typeElement);
}
return super.visitType(typeElement, arg);
}
}
private static final class PackageEntry {
private final String packageName;
private final boolean imported;
public PackageEntry(String packageName, boolean imported) {
this.packageName = packageName;
this.imported = imported;
}
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + packageName.hashCode();
hash = 23 * hash + Boolean.valueOf(imported).hashCode();
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
PackageEntry other = (PackageEntry) obj;
if (!this.packageName.equals(other.packageName)) {
return false;
}
if (this.imported != other.imported) {
return false;
}
return true;
}
}
}