blob: ddd6f29f73c8e6c1412ea89937e7790fefe54c0a [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.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.Rule.SelectionRule;
import org.netbeans.modules.csl.api.*;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.editor.indent.api.Indent;
import org.netbeans.modules.html.editor.api.HtmlKit;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.lib.api.HtmlParsingResult;
import org.netbeans.modules.html.editor.lib.api.elements.CloseTag;
import org.netbeans.modules.html.editor.lib.api.elements.Element;
import org.netbeans.modules.html.editor.lib.api.elements.ElementType;
import org.netbeans.modules.html.editor.lib.api.elements.ElementUtils;
import org.netbeans.modules.html.editor.lib.api.elements.ElementVisitor;
import org.netbeans.modules.html.editor.lib.api.elements.Node;
import org.netbeans.modules.html.editor.lib.api.elements.OpenTag;
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.api.indexing.IndexingManager;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.web.api.webmodule.WebModule;
import org.netbeans.modules.web.common.api.WebUtils;
import org.netbeans.modules.web.jsfapi.api.Library;
import org.netbeans.modules.web.jsfapi.spi.LibraryUtils;
import org.netbeans.spi.editor.codegen.CodeGenerator;
import org.netbeans.spi.project.ui.templates.support.Templates;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.loaders.TemplateWizard;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
/**
*
* @author marekfukala
*/
public class InjectCompositeComponent {
private static final int HINT_PRIORITY = 60; //magic number
private static final String TEMPLATES_FOLDER = "JSF"; //NOI18N
private static final String TEMPLATE_NAME = "out.xhtml"; //NOI18N
public static void inject(Document document, int from, int to) {
try {
FileObject fileObject = NbEditorUtilities.getFileObject(document);
Project project = FileOwnerQuery.getOwner(fileObject);
instantiateTemplate(project, fileObject, document, from, to);
} catch (ParseException | BadLocationException | IOException ex) {
Exceptions.printStackTrace(ex);
}
}
public static Hint getHint(final RuleContext context, final int from, final int to) {
return new Hint(injectCCRule,
NbBundle.getMessage(InjectCompositeComponent.class, "MSG_InjectCompositeComponentSelectionHintDescription"), //NOI18N
context.parserResult.getSnapshot().getSource().getFileObject(),
new OffsetRange(from, to),
Collections.<HintFix>singletonList(new HintFix() {
@Override
public String getDescription() {
return NbBundle.getMessage(InjectCompositeComponent.class, "MSG_InjectCompositeComponentSelectionHintDescription"); //NOI18N
}
@Override
public void implement() throws Exception {
inject(context.doc, from, to);
}
@Override
public boolean isSafe() {
return true;
}
@Override
public boolean isInteractive() {
return true;
}
}),
HINT_PRIORITY);
}
private static void instantiateTemplate(Project project, FileObject file, final Document document, final int startOffset, final int endOffset) throws BadLocationException, DataObjectNotFoundException, IOException, ParseException {
String selectedText = startOffset == endOffset ? null : document.getText(startOffset, endOffset - startOffset);
TemplateWizard templateWizard = new TemplateWizard();
templateWizard.putProperty("project", project); //NOI18N
templateWizard.putProperty("selectedText", selectedText); //NOI18N
templateWizard.setTitle(NbBundle.getMessage(InjectCompositeComponent.class, "MSG_InsertCompositeComponent")); //NOI18N
templateWizard.putProperty("fromEditor", true); //NOI18N
DataFolder templatesFolder = templateWizard.getTemplatesFolder();
FileObject template = templatesFolder.getPrimaryFile().getFileObject(TEMPLATES_FOLDER + "/" + TEMPLATE_NAME); //NOI18N
DataObject templateDO;
FileObject projectDir = project.getProjectDirectory();
DataFolder targetFolder = DataFolder.findFolder(projectDir);
final Logger logger = Logger.getLogger(InjectCompositeComponent.class.getSimpleName());
final JsfSupportImpl jsfs = JsfSupportImpl.findFor(file);
if (jsfs == null) {
logger.log(Level.WARNING, "Cannot find JsfSupport instance for file {0}", file.getPath()); //NOI18N
return;
}
final SnippetContext context = getSnippetContext(document, startOffset, endOffset, jsfs);
if (!context.isValid()) {
templateWizard.putProperty("incorrectActionContext", true); //NOI18N
}
//get list of used declarations, which needs to be passed to the wizard
Source source = Source.create(document);
final AtomicReference<Map<String, String>> declaredPrefixes = new AtomicReference<>();
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
ResultIterator ri = WebUtils.getResultIterator(resultIterator, HtmlKit.HTML_MIME_TYPE);
if (ri != null) {
HtmlParsingResult result = (HtmlParsingResult) ri.getParserResult();
if (result != null) {
declaredPrefixes.set(result.getNamespaces());
}
}
}
});
templateWizard.putProperty("declaredPrefixes", declaredPrefixes.get()); //NOI18N
templateDO = DataObject.find(template);
final Set<DataObject> result = templateWizard.instantiate(templateDO, targetFolder);
final String prefix = (String) templateWizard.getProperty("selectedPrefix"); //NOI18N
if (result != null && result.size() > 0) {
final String compName = result.iterator().next().getName();
final BaseDocument doc = (BaseDocument) document;
final Indent indent = Indent.get(doc);
indent.lock();
try {
doc.runAtomic(new Runnable() {
@Override
public void run() {
try {
doc.remove(startOffset, endOffset - startOffset);
String text = "<" + prefix + ":" + compName + "/>"; //NOI18N
doc.insertString(startOffset, text, null);
indent.reindent(startOffset, startOffset + text.length());
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
});
} finally {
indent.unlock();
}
// get component folder
FileObject tF = Templates.getTargetFolder(templateWizard);
String compFolder = FileUtil.getRelativePath(projectDir, tF);
compFolder = compFolder.substring(compFolder.lastIndexOf("/") + 1);
// issue #232189 - Composite component's NS declaration not inserted on first usage
FileObject generatedFO = result.iterator().next().getPrimaryFile();
if (generatedFO != null) {
WebModule webModule = WebModule.getWebModule(generatedFO);
if (webModule != null && webModule.getDocumentBase() != null) {
IndexingManager.getDefault().refreshIndexAndWait(
webModule.getDocumentBase().toURL(),
// issue #225974 - refresh the source FO to get hints
Arrays.asList(generatedFO.toURL(), source.getFileObject().toURL()));
}
}
//now we need to import the library if not already done,
//but since the library has just been created by adding an xhtml file
//to the resources/xxx/ folder we need to wait until the files
//get indexed and the library is created
final String compositeLibURL = LibraryUtils.getCompositeLibraryURL(compFolder, jsfs.isJsf22Plus());
Source documentSource = Source.create(document);
ParserManager.parseWhenScanFinished(Collections.singletonList(documentSource), new UserTask() { //NOI18N
@Override
public void run(ResultIterator resultIterator) throws Exception {
Library lib = jsfs.getLibrary(compositeLibURL);
if (lib != null) {
if (!LibraryUtils.importLibrary(document, lib, prefix, jsfs.isJsf22Plus())) { //XXX: fix the damned static prefix !!!
logger.log(Level.WARNING, "Cannot import composite components library {0}", compositeLibURL); //NOI18N
}
} else {
//error
logger.log(Level.WARNING, "Composite components library for uri {0} seems not to be created.", compositeLibURL); //NOI18N
}
}
});
//now we need to import all the namespaces refered in the snipet
DataObject templateInstance = result.iterator().next();
final EditorCookie ec = templateInstance.getLookup().lookup(EditorCookie.class);
final Document templateInstanceDoc = ec.openDocument();
ParserManager.parseWhenScanFinished(Collections.singletonList(documentSource), new UserTask() { //NOI18N
@Override
public void run(ResultIterator resultIterator) throws Exception {
final Map<Library, String> importsMap = new LinkedHashMap<>();
for (Map.Entry<String, String> entry : context.getDeclarations().entrySet()) {
String uri = entry.getKey();
String prefix = entry.getValue();
Library lib = jsfs.getLibrary(uri);
if (lib != null) {
importsMap.put(lib, prefix);
}
}
//do the import under atomic lock in different thread,
RequestProcessor.getDefault().post(new Runnable() {
@Override
public void run() {
((BaseDocument) templateInstanceDoc).runAtomic(new Runnable() {
@Override
public void run() {
LibraryUtils.importLibrary(templateInstanceDoc, importsMap, jsfs.isJsf22Plus());
}
});
try {
ec.saveDocument(); //save the template instance after imports
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
}
});
}
});
}
}
private static SnippetContext getSnippetContext(Document doc, final int from, final int to, final JsfSupportImpl jsfs) {
final SnippetContext context = new SnippetContext();
context.setValid(true);
final Source source = Source.create(doc);
try {
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
final HtmlParserResult result = (HtmlParserResult) JsfUtils.getEmbeddedParserResult(resultIterator, "text/html"); //NOI18N
if (result == null) {
return;
}
final int astFrom = result.getSnapshot().getEmbeddedOffset(from);
final int astTo = result.getSnapshot().getEmbeddedOffset(to);
try {
for (final String libUri : result.getNamespaces().keySet()) {
//is the declared uri a faceler library?
if (jsfs.getLibrary(libUri) == null) {
continue; //no facelets stuff, skip it
}
Node root = result.root(libUri);
ElementUtils.visitChildren(
root,
new SnippetContextVisitor(astFrom, astTo, result, context, libUri),
ElementType.OPEN_TAG);
}
} catch (AstTreeVisitingBreakException e) {
//no-op, we just need to stop the visition once we find first problem
context.setValid(false);
}
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
return context;
}
private static final Rule injectCCRule = new InjectCCSelectionRule();
private static class AstTreeVisitingBreakException extends RuntimeException {
};
private static class SnippetContext {
private boolean valid;
private Map<String, String> relatedDeclarations = new HashMap<>();
public void setValid(boolean valid) {
this.valid = valid;
}
public void addDeclaration(String uri, String prefix) {
relatedDeclarations.put(uri, prefix);
}
/**
* uri2prefix map of related declarations
*/
public Map<String, String> getDeclarations() {
return relatedDeclarations;
}
public boolean isValid() {
return valid;
}
}
private static class InjectCCSelectionRule implements SelectionRule {
@Override
public boolean appliesTo(RuleContext context) {
return true;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public boolean showInTasklist() {
return false;
}
@Override
public HintSeverity getDefaultSeverity() {
return HintSeverity.CURRENT_LINE_WARNING; //???
}
}
public static class InjectCCCodeGen implements CodeGenerator {
@Override
public String getDisplayName() {
return NbBundle.getMessage(InjectCompositeComponent.class, "MSG_InjectCompositeComponentHint"); //NOI18N
}
@Override
public void invoke() {
JTextComponent textComponent = EditorRegistry.lastFocusedComponent();
Document doc = textComponent.getDocument();
int from = textComponent.getSelectionStart();
int to = textComponent.getSelectionEnd();
inject(doc, from, to);
}
}
private static class SnippetContextVisitor implements ElementVisitor {
private final int astFrom;
private final int astTo;
private final HtmlParserResult result;
private final SnippetContext context;
private final String libUri;
public SnippetContextVisitor(int astFrom, int astTo, HtmlParserResult result, SnippetContext context, String libUri) {
this.astFrom = astFrom;
this.astTo = astTo;
this.result = result;
this.context = context;
this.libUri = libUri;
}
@Override
public void visit(Element node) {
OpenTag ot = (OpenTag) node;
int node_logical_from = ot.from();
int node_logical_to = ot.semanticEnd();
if (node_logical_from >= astFrom && node_logical_from <= astTo
|| node_logical_to >= astFrom && node_logical_to <= astTo) {
//the node start or end is in the selection
//such info is enough to add the node's namespace
//to the list of future imports for the snippet
context.addDeclaration(libUri, result.getNamespaces().get(libUri));
}
//todo: optimize me a bit please :-)
//the node must either contain both offsets or none
if ((astFrom > node_logical_from && astFrom < node_logical_to && !(astTo > node_logical_from && astTo < node_logical_to))
|| (astTo > node_logical_from && astTo < node_logical_to && !(astFrom > node_logical_from && astFrom < node_logical_to))) {
//crossing tags - quit
fail();
}
//and the offset must not fall into the tag itself
CloseTag closeTag = ot.matchingCloseTag();
if (closeTag == null) {
//broken source, error
if (!ot.isEmpty()) {
fail();
}
}
if (isInTagItself(node, astFrom) || isInTagItself(node, astTo)
|| isInTagItself(closeTag, astFrom) || isInTagItself(closeTag, astTo)) {
fail();
}
}
private boolean isInTagItself(Element node, int offset) {
return node != null && node.from() < offset && node.to() > offset;
}
private void fail() {
context.setValid(false);
throw new AstTreeVisitingBreakException();
}
}
}