blob: 51c889d142b187e570a6b6eddc185c25d908971a [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.ant.grammar;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import org.apache.tools.ant.module.api.IntrospectedInfo;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.modules.xml.api.model.GrammarQuery;
import org.netbeans.modules.xml.api.model.GrammarResult;
import org.netbeans.modules.xml.api.model.HintContext;
import org.netbeans.modules.xml.spi.dom.AbstractNode;
import org.netbeans.modules.xml.api.model.DescriptionSource;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.URLMapper;
import org.openide.util.Enumerations;
import org.openide.util.Lookup;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
/**
* Rather simple query implementation based on static Ant introspection info.
* Hints given by this grammar cannot guarantee that valid XML document is created.
*
* @author Petr Kuzel, Jesse Glick
*/
class AntGrammar implements GrammarQuery {
private static final Logger LOG = Logger.getLogger(AntGrammar.class.getName());
/**
* Allow to get names of <b>parsed general entities</b>.
* @return list of <code>CompletionResult</code>s (ENTITY_REFERENCE_NODEs)
*/
public @Override Enumeration<GrammarResult> queryEntities(String prefix) {
List<GrammarResult> list = new ArrayList<GrammarResult>();
// add well-know build-in entity names
if ("lt".startsWith(prefix)) {
list.add(new MyEntityReference("lt"));
}
if ("gt".startsWith(prefix)) {
list.add(new MyEntityReference("gt"));
}
if ("apos".startsWith(prefix)) {
list.add(new MyEntityReference("apos"));
}
if ("quot".startsWith(prefix)) {
list.add(new MyEntityReference("quot"));
}
if ("amp".startsWith(prefix)) {
list.add(new MyEntityReference("amp"));
}
LOG.log(Level.FINE, "queryEntities({0}) -> {1}", new Object[] {prefix, list});
return Collections.enumeration(list);
}
/*
private static String getTaskClassFor(String elementName) {
Map defs = getAntGrammar().getDefs("task");
return (String) defs.get(elementName);
}
private static String getTypeClassFor(String elementName) {
Map defs = getAntGrammar().getDefs("type");
return (String) defs.get(elementName);
}
*/
private static IntrospectedInfo getAntGrammar() {
return IntrospectedInfo.getKnownInfo();
}
enum Kind {
/** this element is a task */
TASK,
/** this element is a data type */
TYPE,
/** this element is part of some other structure (task or type) */
DATA,
/** tag for root project element */
PROJECT,
/** tag for a target element */
TARGET,
/** tag for a project description element */
DESCRIPTION,
/** tag for an import statement */
IMPORT;
}
static class ElementType {
final Kind kind;
final String name; // null for PROJECT, TARGET, DESCRIPTION, IMPORT
ElementType(Kind kind, String name) {
this.kind = kind;
this.name = name;
}
}
/**
* Determine what a particular element in a build script represents,
* based on its name and the names of all of its parents.
* Returns a pair of the kind of the element (one of the KIND_* constants)
* and the details (a class name suitable for {@link IntrospectedInfo}, or
* in the case of {@link KIND_SPECIAL}, one of the SPECIAL_* constants).
* @param e an element
* @return a two-element string (kind and details), or null if this element is anomalous
*/
static ElementType typeOf(Element e) {
String name = e.getNodeName();
Node p = e.getParentNode();
if (p == null) {
throw new IllegalArgumentException("Detached node: " + e); // NOI18N
}
if (p.getNodeType() == Node.DOCUMENT_NODE) {
if (name.equals("project")) { // NOI18N
return new ElementType(Kind.PROJECT, null);
} else {
// Weird root element? Ignore.
return null;
}
} else if (p.getNodeType() == Node.ELEMENT_NODE) {
// Find ourselves in context.
ElementType ptype = typeOf((Element)p);
if (ptype == null) {
// Unknown parent, therefore this is unknown too.
return null;
}
switch (ptype.kind) {
case PROJECT:
// <project> may have <description>, or types, or targets, or tasks
if (name.equals("description")) { // NOI18N
return new ElementType(Kind.DESCRIPTION, null);
} else if (name.equals("target")) { // NOI18N
return new ElementType(Kind.TARGET, null);
} else if (name.equals("import")) { // NOI18N
return new ElementType(Kind.IMPORT, null);
} else {
String taskClazz = getAntGrammar().getDefs("task").get(name); // NOI18N
if (taskClazz != null) {
return new ElementType(Kind.TASK, taskClazz);
} else {
String typeClazz = getAntGrammar().getDefs("type").get(name); // NOI18N
if (typeClazz != null) {
return new ElementType(Kind.TYPE, typeClazz);
} else {
return null;
}
}
}
case TARGET:
// <target> may have tasks and types
String taskClazz = getAntGrammar().getDefs("task").get(name); // NOI18N
if (taskClazz != null) {
return new ElementType(Kind.TASK, taskClazz);
} else {
String typeClazz = getAntGrammar().getDefs("type").get(name); // NOI18N
if (typeClazz != null) {
return new ElementType(Kind.TYPE, typeClazz);
} else {
return null;
}
}
case DESCRIPTION:
// <description> should have no children!
return null;
case IMPORT:
// <import> should have no children!
return null;
default:
// We must be data.
String clazz = getAntGrammar().isKnown(ptype.name) ? getAntGrammar().getElements(ptype.name).get(name) : null;
if (clazz != null) {
return new ElementType(Kind.DATA, clazz);
} else {
// Unknown data.
return null;
}
}
} else {
throw new IllegalArgumentException("Bad parent for " + e.toString() + ": " + p); // NOI18N
}
}
/**
* @stereotype query
* @output list of results that can be queried on name, and attributes
* @time Performs fast up to 300 ms.
* @param ctx represents virtual attribute <code>Node</code> to be replaced. Its parent is a element node.
* @return list of <code>CompletionResult</code>s (ATTRIBUTE_NODEs) that can be queried on name, and attributes.
* Every list member represents one possibility.
*/
public @Override Enumeration<GrammarResult> queryAttributes(HintContext ctx) {
Element ownerElement = null;
// Support both versions of GrammarQuery contract
if (ctx.getNodeType() == Node.ATTRIBUTE_NODE) {
ownerElement = ((Attr)ctx).getOwnerElement();
} else if (ctx.getNodeType() == Node.ELEMENT_NODE) {
ownerElement = (Element) ctx;
}
if (ownerElement == null) {
return Enumerations.empty();
}
NamedNodeMap existingAttributes = ownerElement.getAttributes();
List<String> possibleAttributes;
ElementType type = typeOf(ownerElement);
if (type == null) {
return Enumerations.empty();
}
switch (type.kind) {
case PROJECT:
possibleAttributes = new LinkedList<String>();
possibleAttributes.add("default");
possibleAttributes.add("name");
possibleAttributes.add("basedir");
break;
case TARGET:
possibleAttributes = new LinkedList<String>();
possibleAttributes.add("name");
possibleAttributes.add("depends");
possibleAttributes.add("description");
possibleAttributes.add("if");
possibleAttributes.add("unless");
break;
case DESCRIPTION:
return Enumerations.empty();
case IMPORT:
possibleAttributes = new LinkedList<String>();
possibleAttributes.add("file");
possibleAttributes.add("optional");
break;
default:
// task, type, or data; anyway, we have the defining class
possibleAttributes = new LinkedList<String>();
if (type.kind == Kind.TYPE) {
possibleAttributes.add("id");
}
if (getAntGrammar().isKnown(type.name)) {
possibleAttributes.addAll(new TreeSet<String>(getAntGrammar().getAttributes(type.name).keySet()));
}
if (type.kind == Kind.TASK) {
// Can have an ID too, but less important; leave at end.
possibleAttributes.add("id");
// Currently IntrospectedInfo includes this in the props for a type,
// though it excludes it for tasks. So for now add it explicitly
// only to tasks.
possibleAttributes.add("description");
// Also useful sometimes:
possibleAttributes.add("taskname");
}
}
String prefix = ctx.getCurrentPrefix();
List<GrammarResult> list = new ArrayList<GrammarResult>();
for (String attribute : possibleAttributes) {
if (attribute.startsWith(prefix)) {
if (existingAttributes.getNamedItem(attribute) == null) {
list.add(new MyAttr(attribute));
}
}
}
LOG.log(Level.FINE, "queryAttributes({0}) -> {1}", new Object[] {prefix, list});
return Collections.enumeration(list);
}
/**
* @semantics Navigates through read-only Node tree to determine context and provide right results.
* @postconditions Let ctx unchanged
* @time Performs fast up to 300 ms.
* @stereotype query
* @param ctx represents virtual element Node that has to be replaced, its own attributes does not name sense, it can be used just as the navigation start point.
* @return list of <code>CompletionResult</code>s (ELEMENT_NODEs) that can be queried on name, and attributes
* Every list member represents one possibility.
*/
public @Override Enumeration<GrammarResult> queryElements(HintContext ctx) {
Node parent = ((Node)ctx).getParentNode();
if (parent == null) {
return Enumerations.empty();
}
if (parent.getNodeType() != Node.ELEMENT_NODE) {
return Enumerations.empty();
}
List<String> elements;
ElementType type = typeOf((Element)parent);
if (type == null) {
return Enumerations.empty();
}
switch (type.kind) {
case PROJECT:
elements = new LinkedList<String>();
elements.add("target");
elements.add("import");
elements.add("property");
elements.add("description");
SortedSet<String> tasks = getSortedDefs("task");
tasks.remove("property");
tasks.remove("import");
elements.addAll(tasks); // Ant 1.6 permits any tasks at top level
elements.addAll(getSortedDefs("type"));
break;
case TARGET:
elements = new ArrayList<String>(getSortedDefs("task"));
// targets can have embedded types too, though less common:
elements.addAll(getSortedDefs("type")); // NOI18N
break;
case DESCRIPTION:
return Enumerations.empty();
case IMPORT:
return Enumerations.empty();
default:
// some introspectable class
if (getAntGrammar().isKnown(type.name)) {
elements = new ArrayList<String>(new TreeSet<String>(getAntGrammar().getElements(type.name).keySet()));
} else {
elements = Collections.emptyList();
}
}
String prefix = ctx.getCurrentPrefix();
List<GrammarResult> list = new ArrayList<GrammarResult>();
for (final String element : elements) {
if (element.startsWith(prefix)) {
switch (type.kind) {
case PROJECT:
case TARGET:
list.add(new MyElement(element) {
private URL manpage;
{
ClassLoader cl = Lookup.getDefault().lookup(ClassLoader.class);
manpage = cl.getResource("org/apache/tools/ant/module/docs/ant-docs/Tasks/" + element + ".html");
if (manpage == null) {
manpage = cl.getResource("org/apache/tools/ant/module/docs/ant-docs/Types/" + element + ".html");
}
}
@Override
public URL getContentURL() {
return manpage;
}
});
break;
default:
list.add(new MyElement(element));
}
}
}
LOG.log(Level.FINE, "queryElements({0}) -> {1}", new Object[] {prefix, list});
return Collections.enumeration(list);
}
private static SortedSet<String> getSortedDefs(String kind) {
SortedSet<String> defs = new TreeSet<String>(Collator.getInstance());
defs.addAll(getAntGrammar().getDefs(kind).keySet());
return defs;
}
/**
* Allow to get names of <b>declared notations</b>.
* @return list of <code>CompletionResult</code>s (NOTATION_NODEs)
*/
public @Override Enumeration<GrammarResult> queryNotations(String prefix) {
return Enumerations.empty();
}
public @Override Enumeration<GrammarResult> queryValues(HintContext ctx) {
LOG.log(Level.FINE, "queryValues({0})", ctx.getCurrentPrefix());
// #38341: ctx is apparently instanceof Attr or Text
// (actually never instanceof Text, just TEXT_NODE: #38339)
Attr ownerAttr;
if (canCompleteProperty(ctx.getCurrentPrefix())) {
LOG.fine("...can complete property");
return completeProperties(ctx);
} else if (ctx.getNodeType() == Node.ATTRIBUTE_NODE) {
ownerAttr = (Attr)ctx;
} else {
LOG.fine("...unknown node type");
return Enumerations.empty();
}
Element ownerElement = ownerAttr.getOwnerElement();
String attrName = ownerAttr.getName();
ElementType type = typeOf(ownerElement);
if (type == null) {
LOG.fine("...unknown type");
return Enumerations.empty();
}
List<String> choices = new ArrayList<String>();
switch (type.kind) {
case PROJECT:
if (attrName.equals("default")) {
// XXX list known targets?
} else if (attrName.equals("basedir")) {
// XXX file completion?
}
// freeform: name
break;
case TARGET:
if (attrName.equals("depends")) {
// XXX list known targets?
} else if (attrName.equals("if") || attrName.equals("unless")) {
choices.addAll(Arrays.asList(likelyPropertyNames(ctx)));
}
// freeform: description
break;
case DESCRIPTION:
// nothing applicable
break;
case IMPORT:
if (attrName.equals("file")) {
// freeform
} else if (attrName.equals("optional")) {
choices.add("true");
choices.add("false");
}
break;
default:
String elementClazz = type.name;
if (getAntGrammar().isKnown(elementClazz)) {
String attrClazzName = getAntGrammar().getAttributes(elementClazz).get(attrName);
if (attrClazzName != null) {
if (getAntGrammar().isKnown(attrClazzName)) {
String[] enumTags = getAntGrammar().getTags(attrClazzName);
if (enumTags != null) {
choices.addAll(Arrays.asList(enumTags));
}
}
if (attrClazzName.equals("boolean")) {
choices.add("true");
choices.add("false");
} else if (attrClazzName.equals("org.apache.tools.ant.types.Reference")) {
// XXX add names of ids
} else if (attrClazzName.equals("org.apache.tools.ant.types.Path") ||
attrClazzName.equals("java.io.File")
/* || "path" attr on Path or Path.Element */
) {
// XXX complete filenames
} else if (attrClazzName.equals("java.lang.String") &&
Arrays.asList(PROPERTY_NAME_VALUED_PROPERTY_NAMES).contains(attrName)) {
// <isset property="..."/>, <include name="*" unless="..."/>, etc.
choices.addAll(Arrays.asList(likelyPropertyNames(ctx)));
}
}
}
}
// Create the completion:
String prefix = ctx.getCurrentPrefix();
List<GrammarResult> list = new ArrayList<GrammarResult>();
for (String choice : choices) {
if (choice.startsWith(prefix)) {
list.add (new MyText(choice, choice, -1));
}
}
LOG.log(Level.FINE, "queryValues({0}) -> {1}", new Object[] {prefix, list});
return Collections.enumeration(list);
}
/**
* Check whether a given content string (of an attribute value or of an element's
* content) has an uncompleted "${" sequence in it, i.e. one that has not been matched
* with a corresponding "}".
* E.g.:
* <pathelement location="${foo
* ^ caret
* Also if the last character is "$" it can be completed.
* @param content the current content of the attribute value or element
* @return true if there is an uncompleted property here
*/
private static boolean canCompleteProperty(String content) {
content = deletedEscapedShells(content);
if (content.length() == 0) {
return false;
}
if (content.charAt(content.length() - 1) == '$') {
return true;
}
int idx = content.lastIndexOf("${");
return idx != -1 && content.indexOf('}', idx) == -1;
}
private static Enumeration<GrammarResult> completeProperties(HintContext ctx) {
String content = ctx.getCurrentPrefix();
assert content.length() > 0;
String header;
String propPrefix;
String wholeText = ctx.getNodeValue();
int replaceLen;
String suffix = "";
if (content.charAt(content.length() - 1) == '$') { // NOI18N
header = content + '{';
propPrefix = "";
// no closing brace, since even opening brace is missing
replaceLen = 1;
} else {
int idx = content.lastIndexOf("${"); // NOI18N
assert idx != -1;
header = content.substring(0, idx + 2);
propPrefix = content.substring(idx + 2);
int closingBrace = wholeText.indexOf('}', content.length()); // NOI18N
if (closingBrace == -1) {
replaceLen = content.length();
suffix = wholeText.substring(replaceLen);
} else {
// only delete the text up to the closing brace
replaceLen = closingBrace + 1;
}
}
String[] props = likelyPropertyNames(ctx);
// completion on text works differently from attrs:
// the context should not be returned (#38342)
boolean shortHeader = ctx.getNodeType() == Node.TEXT_NODE;
List<GrammarResult> list = new ArrayList<GrammarResult>();
int pl = propPrefix.length();
for (int i = 0; i < props.length; i++) {
if (props[i].startsWith(propPrefix)) {
String text = header + props[i] + '}';
String all = "${" + props[i] + "}";
int l = 0;
if (shortHeader) {
assert text.startsWith(content) : "text=" + text + " content=" + content;
text = text.substring(content.length());
}
for (; l < suffix.length() && l + pl < props[i].length(); l++) {
if (suffix.charAt(l) != props[i].charAt(l + pl)) {
break;
}
}
if (shortHeader && (content.length() <= replaceLen)) {
l = -content.length();
}
replaceLen += l;
list.add(new MyText(text, all, replaceLen));
}
}
LOG.log(Level.FINE, "completeProperties({0}) -> {1}", new Object[] {content, list});
return Collections.enumeration(list);
}
/**
* Names of Ant properties that are generally present and defined in any script.
*/
private static final String[] STOCK_PROPERTY_NAMES = {
// Present in most Ant installations:
"ant.home", // NOI18N
// Defined by Ant as standard properties:
"basedir", // NOI18N
"ant.file", // NOI18N
"ant.project.name", // NOI18N
"ant.java.version", // NOI18N
"ant.version", // NOI18N
// Defined by System.getProperties as standard system properties:
"java.version", // NOI18N
"java.vendor", // NOI18N
"java.vendor.url", // NOI18N
"java.home", // NOI18N
"java.vm.specification.version", // NOI18N
"java.vm.specification.vendor", // NOI18N
"java.vm.specification.name", // NOI18N
"java.vm.version", // NOI18N
"java.vm.vendor", // NOI18N
"java.vm.name", // NOI18N
"java.specification.version", // NOI18N
"java.specification.vendor", // NOI18N
"java.specification.name", // NOI18N
"java.class.version", // NOI18N
"java.class.path", // NOI18N
"java.library.path", // NOI18N
"java.io.tmpdir", // NOI18N
"java.compiler", // NOI18N
"java.ext.dirs", // NOI18N
"os.name", // NOI18N
"os.arch", // NOI18N
"os.version", // NOI18N
"file.separator", // NOI18N
"path.separator", // NOI18N
"line.separator", // NOI18N
"user.name", // NOI18N
"user.home", // NOI18N
"user.dir", // NOI18N
};
private static String[] likelyPropertyNames(HintContext ctx) {
// #38343: ctx.getOwnerDocument returns some bogus unusable empty thing
// so find the root element manually
Element parent;
// #38341: docs for queryValues says Attr or Element, but really Attr or Text
// (and CDataSection never seems to permit completion at all...)
if (ctx.getNodeType() == Node.ATTRIBUTE_NODE) {
parent = ((Attr)ctx).getOwnerElement();
} else if (ctx.getNodeType() == Node.TEXT_NODE) {
Node p = ctx.getParentNode();
if (p == null) {
return new String[0];
} else if (p.getNodeType() == Node.ELEMENT_NODE) {
parent = (Element)p;
} else {
LOG.log(Level.WARNING, "strange parent of text node: {0} {1}", new Object[] {p.getNodeType(), p});
return new String[0];
}
} else {
LOG.log(Level.WARNING, "strange context type: {0} {1}", new Object[] {ctx.getNodeType(), ctx});
return new String[0];
}
while (parent.getParentNode() != null && parent.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
parent = (Element)parent.getParentNode();
}
// #38343: getElementsByTagName just throws an exception, you can't use it...
Set<String> choices = new TreeSet<String>(Arrays.asList(STOCK_PROPERTY_NAMES));
visitForLikelyPropertyNames(parent, choices);
Iterator<String> it = choices.iterator();
while (it.hasNext()) {
String propname = it.next();
if (propname.indexOf("${") != -1) {
// Not actually a direct property name, rather a computed name.
// Skip it as it cannot be used here.
it.remove();
}
}
return choices.toArray(new String[choices.size()]);
}
private static final String[] PROPERTY_NAME_VALUED_PROPERTY_NAMES = {
"if",
"unless",
// XXX accept any *property
"property",
"failureproperty",
"errorproperty",
"addproperty",
};
private static void visitForLikelyPropertyNames(Node n, Set<String> choices) {
int type = n.getNodeType();
switch (type) {
case Node.ELEMENT_NODE:
// XXX would be more precise to use typeOf here, but maybe slower?
// Look for <property name="propname" .../> and similar
Element el = (Element)n;
String tagname = el.getTagName();
if (tagname.equals("property")) {
String propname = el.getAttribute("name");
// #38343: Element impl is broken and can return null from getAttribute
if (propname != null && propname.length() > 0) {
choices.add(propname);
}
// XXX handle <property file="..."/> with a resolvable filename
} else if (tagname.equals("buildnumber")) {
// This task always defines ${build.number}
choices.add("build.number");
} else if (tagname.equals("tstamp")) {
// XXX handle prefix="whatever" -> ${whatever.TODAY} etc.
// XXX handle nested <format property="foo" .../> -> ${foo}
choices.add("DSTAMP");
choices.add("TSTAMP");
choices.add("TODAY");
}
// <available>, <dirname>, <pathconvert>, <uptodate>, <target>, <isset>, <include>, etc.
for (int i = 0; i < PROPERTY_NAME_VALUED_PROPERTY_NAMES.length; i++) {
String propname = el.getAttribute(PROPERTY_NAME_VALUED_PROPERTY_NAMES[i]);
if (propname != null && propname.length() > 0) {
choices.add(propname);
}
}
break;
case Node.ATTRIBUTE_NODE:
case Node.TEXT_NODE:
// Look for ${propname}
String text = deletedEscapedShells(n.getNodeValue());
int idx = 0;
while (true) {
int start = text.indexOf("${", idx);
if (start == -1) {
break;
}
int end = text.indexOf('}', start + 2);
if (end == -1) {
break;
}
String propname = text.substring(start + 2, end);
if (propname.length() > 0) {
choices.add(propname);
}
idx = end + 1;
}
break;
default:
// ignore
break;
}
NodeList l = n.getChildNodes();
for (int i = 0; i < l.getLength(); i++) {
visitForLikelyPropertyNames(l.item(i), choices);
}
// Element attributes are not considered child nodes as such.
NamedNodeMap m = n.getAttributes();
if (m != null) {
for (int i = 0; i < m.getLength(); i++) {
visitForLikelyPropertyNames(m.item(i), choices);
}
}
}
/**
* Remove pairs of '$$' to avoid being confused by them.
* They do not introduce property references.
*/
private static String deletedEscapedShells(String text) {
// XXX could be faster w/o regexps
return text.replaceAll("\\$\\$", "");
}
// return defaults, no way to query them
public @Override GrammarResult queryDefault(final HintContext ctx) {
return null;
}
// it is not yet implemented
public @Override boolean isAllowed(Enumeration<GrammarResult> en) {
return true;
}
// customizers section ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public @Override java.awt.Component getCustomizer(HintContext ctx) {
return null;
}
public @Override boolean hasCustomizer(HintContext ctx) {
return false;
}
public @Override org.openide.nodes.Node.Property<?>[] getProperties(HintContext ctx) {
return null;
}
// Result classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private static abstract class AbstractResultNode extends AbstractNode implements GrammarResult {
public @Override Icon getIcon(int kind) {
return null;
}
public @Override String getDescription() {
return null;
}
public @Override String getDisplayName() {
return null;
}
// TODO in MyElement return true for really empty elements such as "pathelement"
public @Override boolean isEmptyElement() {
return false;
}
}
private static class MyEntityReference extends AbstractResultNode implements EntityReference {
private String name;
MyEntityReference(String name) {
this.name = name;
}
public @Override short getNodeType() {
return Node.ENTITY_REFERENCE_NODE;
}
public @Override String getNodeName() {
return name;
}
}
private static class MyElement extends AbstractResultNode implements Element, DescriptionSource {
private final String name;
MyElement(String name) {
this.name = name;
}
public @Override short getNodeType() {
return Node.ELEMENT_NODE;
}
public @Override String getNodeName() {
return name;
}
public @Override String getTagName() {
return name;
}
public @Override String toString() {
return name;
}
@Override
public DescriptionSource resolveLink(String link) {
return null;
}
@Override
public boolean isExternal() {
return false;
}
@Override
public URL getContentURL() {
return null;
}
}
private static class MyAttr extends AbstractResultNode implements Attr {
private final String name;
MyAttr(String name) {
this.name = name;
}
public @Override short getNodeType() {
return Node.ATTRIBUTE_NODE;
}
public @Override String getNodeName() {
return name;
}
public @Override String getName() {
return name;
}
public @Override String getValue() {
return null; //??? what spec says
}
public @Override String toString() {
return name;
}
}
private static class MyText extends AbstractResultNode implements Text {
private final String data;
private final String displayName;
private final int replace;
MyText(String data, String displayName, int replace) {
this.data = data;
this.displayName = displayName;
this.replace = replace;
}
public @Override short getNodeType() {
return Node.TEXT_NODE;
}
public @Override String getNodeValue() {
return data;
}
public @Override String getData() throws DOMException {
return data;
}
public @Override int getLength() {
// if (replace != -1) {
// return replace;
// }
// return data == null ? -1 : data.length();
return replace;
}
public @Override String toString() {
return data;
}
public @Override String getDisplayName() {
return displayName;
}
}
}