| /* |
| * 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; |
| } |
| |
| } |
| |
| } |