blob: 88dd089056d5a25fd7014d53983d15c9baa3b7a4 [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.jsf.editor;
import java.util.Map.Entry;
import java.util.*;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.html.lexer.HTMLTokenId;
import org.netbeans.api.lexer.*;
import org.netbeans.lib.editor.util.CharSequenceUtilities;
import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
import org.netbeans.modules.csl.api.Error;
import org.netbeans.modules.csl.api.HintsProvider.HintsManager;
import org.netbeans.modules.csl.api.*;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.editor.NbEditorDocument;
import org.netbeans.modules.html.editor.api.gsf.HtmlExtension;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.lib.api.elements.*;
import org.netbeans.modules.parsing.api.*;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.modules.web.jsf.api.editor.JsfFacesComponentsProvider.FacesComponentLibrary;
import org.netbeans.modules.web.jsf.editor.completion.JsfAttributesCompletionHelper;
import org.netbeans.modules.web.jsf.editor.completion.JsfCompletionItem;
import org.netbeans.modules.web.jsf.editor.facelets.AbstractFaceletsLibrary;
import org.netbeans.modules.web.jsf.editor.facelets.CompositeComponentLibrary;
import org.netbeans.modules.web.jsf.editor.hints.HintsRegistry;
import org.netbeans.modules.web.jsfapi.api.DefaultLibraryInfo;
import org.netbeans.modules.web.jsfapi.api.Library;
import org.netbeans.modules.web.jsfapi.api.LibraryComponent;
import org.netbeans.modules.web.jsfapi.api.NamespaceUtils;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.netbeans.spi.lexer.MutableTextInput;
/**
* XXX should be rather done by dynamic artificial embedding creation. The
* support then can be implemented by CSL language mapped to the language
* mimetype.
*
* @author marekfukala
*/
@MimeRegistration(mimeType=org.netbeans.modules.web.jsfapi.api.JsfUtils.JSF_XHTML_FILE_MIMETYPE, service=HtmlExtension.class)
public class JsfHtmlExtension extends HtmlExtension {
private static final String EL_ENABLED_KEY = "el_enabled"; //NOI18N
@Override
public Map<OffsetRange, Set<ColoringAttributes>> getHighlights(HtmlParserResult result, SchedulerEvent event) {
final Map<OffsetRange, Set<ColoringAttributes>> highlights = new HashMap<>();
//highlight JSF tags
highlightJsfTags(result, highlights);
//check if the EL is enabled in the file and enables it if not
checkELEnabled(result);
return highlights;
}
public void checkELEnabled(HtmlParserResult result) {
Document doc = result.getSnapshot().getSource().getDocument(true);
InputAttributes inputAttributes = (InputAttributes) doc.getProperty(InputAttributes.class);
if (inputAttributes == null) {
inputAttributes = new InputAttributes();
doc.putProperty(InputAttributes.class, inputAttributes);
}
Language xhtmlLang = Language.find(JsfUtils.XHTML_MIMETYPE); //NOI18N
if (inputAttributes.getValue(LanguagePath.get(xhtmlLang), EL_ENABLED_KEY) == null) {
inputAttributes.setValue(LanguagePath.get(xhtmlLang), EL_ENABLED_KEY, new Object(), false);
//refresh token hierarchy so the EL becomes lexed
recolor(doc);
}
}
private void recolor(final Document doc) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
NbEditorDocument nbdoc = (NbEditorDocument) doc;
nbdoc.extWriteLock();
try {
MutableTextInput mti = (MutableTextInput) doc.getProperty(MutableTextInput.class);
if (mti != null) {
mti.tokenHierarchyControl().rebuild();
}
} finally {
nbdoc.extWriteUnlock();
}
}
});
}
private void highlightJsfTags(HtmlParserResult result, final Map<OffsetRange, Set<ColoringAttributes>> highlights) {
final Snapshot snapshot = result.getSnapshot();
Source source = snapshot.getSource();
JsfSupportImpl jsfs = JsfSupportImpl.findFor(source);
if (jsfs == null) {
return;
}
Map<String, Library> libs = jsfs.getLibraries();
Map<String, String> nss = result.getNamespaces();
//1. resolve which declared libraries are available on classpath
//2. resolve which tag prefixes are registered for libraries, either available or missing
// add hint for missing library
for (String namespace : nss.keySet()) {
Node root = result.root(namespace);
if (root != null) {
final Library tldl = NamespaceUtils.getForNs(libs, namespace);
ElementUtils.visitChildren(root, new ElementVisitor() {
@Override
public void visit(Element element) {
if (element.type() == ElementType.OPEN_TAG
|| element.type() == ElementType.CLOSE_TAG) {
Named named = (Named) element;
if (named.namespacePrefix() != null) {
Set<ColoringAttributes> coloring = tldl == null ? ColoringAttributes.CLASS_SET : ColoringAttributes.METHOD_SET;
try {
highlight(snapshot, named, highlights, coloring);
} catch (BadLocationException ex) {
//just ignore
}
}
}
}
});
}
}
}
private void highlight(Snapshot s, Named node, Map<OffsetRange, Set<ColoringAttributes>> hls, Set<ColoringAttributes> cas) throws BadLocationException {
// "<div" id='x'> part
int prefixLen = node.type() == ElementType.OPEN_TAG ? 1 : 2; //"<" open; "</" close
hls.put(getDocumentOffsetRange(s, node.from(), node.from() + node.name().length() + prefixLen /* tag open symbol len */),
cas);
// <div id='x'">" part
hls.put(getDocumentOffsetRange(s, node.to() - 1, node.to()),
cas);
}
private OffsetRange getDocumentOffsetRange(Snapshot s, int astFrom, int astTo) throws BadLocationException {
int from = s.getOriginalOffset(astFrom);
int to = s.getOriginalOffset(astTo);
if (from == -1 || to == -1) {
throw new BadLocationException("Cannot convert snapshot offset to document offset", -1); //NOI18N
}
return new OffsetRange(from, to);
}
@Override
public List<CompletionItem> completeOpenTags(CompletionContext context) {
HtmlParserResult result = context.getResult();
Source source = result.getSnapshot().getSource();
JsfSupportImpl jsfs = JsfSupportImpl.findFor(source);
if (jsfs == null) {
return Collections.emptyList();
}
Map<String, Library> libs = jsfs.getLibraries();
Set<Library> librariesSet = new HashSet<>(libs.values());
//uri to prefix map
Map<String, String> declaredNS = result.getNamespaces();
List<CompletionItem> items = new ArrayList<>();
int colonIndex = context.getPrefix().indexOf(':');
if (colonIndex == -1) {
//editing namespace or tag w/o ns
//offer all tags
for (Library lib : librariesSet) {
String declaredPrefix = NamespaceUtils.getForNs(declaredNS, lib.getNamespace());
if (declaredPrefix == null) {
//undeclared prefix, try to match with default library prefix
if (lib.getDefaultPrefix() != null && lib.getDefaultPrefix().startsWith(context.getPrefix())) {
items.addAll(queryLibrary(context, lib, lib.getDefaultPrefix(), true, jsfs.isJsf22Plus()));
}
} else {
items.addAll(queryLibrary(context, lib, declaredPrefix, false, jsfs.isJsf22Plus()));
}
}
} else {
String tagNamePrefix = context.getPrefix().substring(0, colonIndex);
//find a namespace according to the prefix
String namespace = getUriForPrefix(tagNamePrefix, declaredNS);
if (namespace == null) {
//undeclared prefix, check if a taglib contains it as
//default prefix. If so, offer it in the cc w/ tag autoimport function
for (Library lib : librariesSet) {
if (lib.getDefaultPrefix() != null && lib.getDefaultPrefix().equals(tagNamePrefix)) {
//match
items.addAll(queryLibrary(context, lib, tagNamePrefix, true, jsfs.isJsf22Plus()));
}
}
} else {
//query only associated lib
Library lib = NamespaceUtils.getForNs(libs, namespace);
if (lib == null) {
//no such lib, exit
return Collections.emptyList();
} else {
//query the library
items.addAll(queryLibrary(context, lib, tagNamePrefix, false, jsfs.isJsf22Plus()));
}
}
}
//filter the items according to the prefix
Iterator<CompletionItem> itr = items.iterator();
while (itr.hasNext()) {
if (!CharSequenceUtilities.startsWith(itr.next().getInsertPrefix(), context.getPrefix())) {
itr.remove();
}
}
return items;
}
private String getUriForPrefix(String prefix, Map<String, String> namespaces) {
for (Entry<String, String> entry : namespaces.entrySet()) {
if (prefix.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
private Collection<CompletionItem> queryLibrary(CompletionContext context, Library lib, String nsPrefix, boolean undeclared, boolean isJsf22Plus) {
Collection<CompletionItem> items = new ArrayList<>();
for (LibraryComponent component : lib.getComponents()) {
if (!(component instanceof AbstractFaceletsLibrary.Function)) {
items.add(JsfCompletionItem.createTag(context.getCCItemStartOffset(), component, nsPrefix, undeclared, isJsf22Plus));
}
}
return items;
}
@Override
public List<CompletionItem> completeAttributes(CompletionContext context) {
HtmlParserResult result = context.getResult();
Source source = result.getSnapshot().getSource();
JsfSupportImpl jsfs = JsfSupportImpl.findFor(source);
if (jsfs == null) {
return Collections.emptyList();
}
Map<String, Library> libs = jsfs.getLibraries();
//uri to prefix map
Map<String, String> declaredNS = result.getNamespaces();
List<CompletionItem> items = new ArrayList<>();
Element queriedNode = context.getCurrentNode();
if (queriedNode.type() != ElementType.OPEN_TAG) {
return Collections.emptyList();
}
OpenTag ot = (OpenTag) queriedNode;
CharSequence nsPrefix = ot.namespacePrefix();
if (nsPrefix == null) {
// this must be at attribute from JSF namespace or not JSF tag (without the prefiex)
String jsfPrefix = declaredNS.get(DefaultLibraryInfo.JSF.getNamespace());
if (context.getItemText().startsWith(jsfPrefix + ":")) { //NOI18N
Library htmlLibrary = NamespaceUtils.getForNs(libs, DefaultLibraryInfo.HTML.getNamespace());
return JsfAttributesCompletionHelper.getJsfItemsForHtmlElement(context, htmlLibrary, context.getItemText());
} else {
return Collections.emptyList();
}
}
String tagName = ot.unqualifiedName().toString();
String namespace = getUriForPrefix(nsPrefix.toString(), declaredNS);
Library flib = null;
if (namespace == null) {
// the namespace is not imported, try find library according the default prefix
for (Library lib : libs.values()) {
if (lib.getDefaultPrefix() != null && lib.getDefaultPrefix().equals(nsPrefix.toString())) {
flib = lib;
break;
}
}
} else {
flib = NamespaceUtils.getForNs(libs, namespace);
}
if (flib == null) {
//The facelets library not found. This happens if one declares
//a namespace which is not matched to any existing library
return Collections.emptyList();
}
JsfAttributesCompletionHelper.completeAttributes(context, items, "", flib, tagName, context.getPrefix());
return items;
}
@Override
public List<CompletionItem> completeAttributeValue(CompletionContext context) {
List<CompletionItem> items = new ArrayList<>();
JsfSupportImpl jsfs = JsfSupportImpl.findFor(context.getResult().getSnapshot().getSource());
String ns = ElementUtils.getNamespace(context.getCurrentNode());
OpenTag openTag = context.getCurrentNode().type() == ElementType.OPEN_TAG
? (OpenTag) context.getCurrentNode() : null;
//complete xmlns attribute value
if(jsfs != null) {
JsfAttributesCompletionHelper.completeXMLNSAttribute(context, items, jsfs);
}
if(ns == null || openTag == null) {
return items;
}
//first try to complete using special metadata
JsfAttributesCompletionHelper.completeTagLibraryMetadata(context, items, ns, openTag);
if(jsfs == null) {
return items;
}
//then try to complete according to the attribute type (taken from the library descriptor)
JsfAttributesCompletionHelper.completeValueAccordingToType(context, items, ns, openTag, jsfs);
// completion for files in cases of ui:include src attribute
JsfAttributesCompletionHelper.completeFaceletsFromProject(context, items, ns, openTag);
// completion for sections in cases of ui:define name attribute
JsfAttributesCompletionHelper.completeSectionsOfTemplate(context, items, ns, openTag);
// completion for java classes in <cc:attribute type="com.example.|
JsfAttributesCompletionHelper.completeJavaClasses(context, items, ns, openTag);
//facets
JsfAttributesCompletionHelper.completeFacetsInCCImpl(context, items, ns, openTag, jsfs);
JsfAttributesCompletionHelper.completeFacets(context, items, ns, openTag, jsfs);
return items;
}
@Override
public DeclarationLocation findDeclaration(ParserResult result, final int caretOffset) {
assert result instanceof HtmlParserResult;
HtmlParserResult htmlresult = (HtmlParserResult) result;
int embeddedOffset = result.getSnapshot().getEmbeddedOffset(caretOffset);
if (embeddedOffset == -1) {
return DeclarationLocation.NONE;
}
Element leaf = htmlresult.findByPhysicalRange(embeddedOffset, true);
if (leaf == null || leaf.type() != ElementType.OPEN_TAG) {
return DeclarationLocation.NONE;
}
JsfSupportImpl jsfs = JsfSupportImpl.findFor(result.getSnapshot().getSource());
if (jsfs == null) {
return DeclarationLocation.NONE;
}
String ns = ElementUtils.getNamespace(leaf);
if (ns == null) {
return DeclarationLocation.NONE;
}
Library lib = jsfs.getLibrary(ns);
if (lib == null) {
return DeclarationLocation.NONE;
}
TokenSequence ts = JsfNavigationHelper.getTokenSequenceAtCaret(result.getSnapshot().getTokenHierarchy(), embeddedOffset);
if (ts == null) {
return DeclarationLocation.NONE;
}
Token t = ts.token();
if (t.id() == HTMLTokenId.VALUE) {
String value = CharSequenceUtilities.toString(ts.token().text()).replaceAll("[\"']", ""); //NOI18N
String attribute = ""; //NOI18N
while (ts.movePrevious()) {
if (ts.token().id() == HTMLTokenId.TAG_OPEN) {
String tag = CharSequenceUtilities.toString(ts.token().text());
return JsfNavigationHelper.goToReferencedFile(htmlresult, embeddedOffset, tag, attribute, value);
} else if (ts.token().id() == HTMLTokenId.ARGUMENT && attribute.isEmpty()) {
attribute = CharSequenceUtilities.toString(ts.token().text());
}
}
} else {
if (lib instanceof CompositeComponentLibrary) {
return JsfNavigationHelper.goToCompositeComponentLibrary(htmlresult, embeddedOffset, lib);
} else if (lib instanceof FacesComponentLibrary) {
return JsfNavigationHelper.goToFacesComponentLibrary(htmlresult, embeddedOffset, (FacesComponentLibrary) lib);
}
}
return DeclarationLocation.NONE;
}
@Override
public OffsetRange getReferenceSpan(final Document doc, final int caretOffset) {
TokenHierarchy th = TokenHierarchy.get(doc);
TokenSequence ts = JsfNavigationHelper.getTokenSequenceAtCaret(th, caretOffset);
if (ts == null) {
return OffsetRange.NONE;
}
Token t = ts.token();
if (t.id() == HTMLTokenId.TAG_OPEN) {
if (CharSequenceUtilities.indexOf(t.text(), ':') != -1) {
return new OffsetRange(ts.offset(), ts.offset() + t.length());
}
} else if (t.id() == HTMLTokenId.ARGUMENT) {
int from = ts.offset();
int to = from + t.text().length();
//try to find the tag and check if there is a prefix
while (ts.movePrevious()) {
if (ts.token().id() == HTMLTokenId.TAG_OPEN) {
if (CharSequenceUtilities.indexOf(ts.token().text(), ':') != -1) {
return new OffsetRange(from, to);
} else {
break;
}
}
}
} else if (t.id() == HTMLTokenId.VALUE) {
CharSequence value = ts.token().text();
int from = ts.offset();
int to = from + t.text().length();
//try to find the tag and check if there is a prefix
while (ts.movePrevious()) {
if (ts.token().id() == HTMLTokenId.TAG_OPEN) {
if (CharSequenceUtilities.indexOf(ts.token().text(), "include") != -1) {
if (CharSequenceUtilities.indexOf(value, "'") != -1 || CharSequenceUtilities.indexOf(value, "\"") != -1) {
from++; to--;
}
return new OffsetRange(from, to);
}
break;
}
}
}
return OffsetRange.NONE;
}
@Override
public void computeErrors(HintsManager manager, RuleContext context, List<Hint> hints, List<Error> unhandled) {
//just delegate to the hints registry and add all gathered results
hints.addAll(HintsRegistry.getDefault().gatherHints(context));
}
@Override
public void computeSelectionHints(HintsManager manager, RuleContext context, List<Hint> hints, int start, int end) {
//inject composite component support
Hint injectCC = InjectCompositeComponent.getHint(context, start, end);
if (injectCC != null) {
hints.add(injectCC);
}
}
}