001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.ant.grammar;
043:
044: import java.text.Collator;
045: import java.util.ArrayList;
046: import java.util.Arrays;
047: import java.util.Collections;
048: import java.util.Enumeration;
049: import java.util.Iterator;
050: import java.util.LinkedList;
051: import java.util.List;
052: import java.util.Set;
053: import java.util.SortedSet;
054: import java.util.TreeSet;
055: import java.util.logging.Level;
056: import java.util.logging.Logger;
057: import javax.swing.Icon;
058: import org.apache.tools.ant.module.api.IntrospectedInfo;
059: import org.netbeans.modules.xml.api.model.GrammarQuery;
060: import org.netbeans.modules.xml.api.model.GrammarResult;
061: import org.netbeans.modules.xml.api.model.HintContext;
062: import org.netbeans.modules.xml.spi.dom.AbstractNode;
063: import org.openide.util.Enumerations;
064: import org.w3c.dom.Attr;
065: import org.w3c.dom.DOMException;
066: import org.w3c.dom.Element;
067: import org.w3c.dom.EntityReference;
068: import org.w3c.dom.NamedNodeMap;
069: import org.w3c.dom.Node;
070: import org.w3c.dom.NodeList;
071: import org.w3c.dom.Text;
072:
073: /**
074: * Rather simple query implemetation based on static Ant introspection info.
075: * Hints given by this grammar cannot guarantee that valid XML document is created.
076: *
077: * @author Petr Kuzel, Jesse Glick
078: */
079: class AntGrammar implements GrammarQuery {
080:
081: private static final Logger LOG = Logger.getLogger(AntGrammar.class
082: .getName());
083:
084: /**
085: * Allow to get names of <b>parsed general entities</b>.
086: * @return list of <code>CompletionResult</code>s (ENTITY_REFERENCE_NODEs)
087: */
088: public Enumeration<GrammarResult> queryEntities(String prefix) {
089: List<GrammarResult> list = new ArrayList<GrammarResult>();
090:
091: // add well-know build-in entity names
092:
093: if ("lt".startsWith(prefix))
094: list.add(new MyEntityReference("lt"));
095: if ("gt".startsWith(prefix))
096: list.add(new MyEntityReference("gt"));
097: if ("apos".startsWith(prefix))
098: list.add(new MyEntityReference("apos"));
099: if ("quot".startsWith(prefix))
100: list.add(new MyEntityReference("quot"));
101: if ("amp".startsWith(prefix))
102: list.add(new MyEntityReference("amp"));
103:
104: LOG.log(Level.FINE, "queryEntities({0}) -> {1}", new Object[] {
105: prefix, list });
106: return Collections.enumeration(list);
107: }
108:
109: /*
110: private static String getTaskClassFor(String elementName) {
111: Map defs = getAntGrammar().getDefs("task");
112: return (String) defs.get(elementName);
113: }
114:
115: private static String getTypeClassFor(String elementName) {
116: Map defs = getAntGrammar().getDefs("type");
117: return (String) defs.get(elementName);
118: }
119: */
120:
121: private static IntrospectedInfo getAntGrammar() {
122: return IntrospectedInfo.getKnownInfo();
123: }
124:
125: enum Kind {
126: /** this element is a task */
127: TASK,
128: /** this element is a data type */
129: TYPE,
130: /** this element is part of some other structure (task or type) */
131: DATA,
132: /** tag for root project element */
133: PROJECT,
134: /** tag for a target element */
135: TARGET,
136: /** tag for a project description element */
137: DESCRIPTION,
138: /** tag for an import statement */
139: IMPORT;
140: }
141:
142: static class ElementType {
143: final Kind kind;
144: final String name; // null for PROJECT, TARGET, DESCRIPTION, IMPORT
145:
146: ElementType(Kind kind, String name) {
147: this .kind = kind;
148: this .name = name;
149: }
150: }
151:
152: /**
153: * Determine what a particular element in a build script represents,
154: * based on its name and the names of all of its parents.
155: * Returns a pair of the kind of the element (one of the KIND_* constants)
156: * and the details (a class name suitable for {@link IntrospectedInfo}, or
157: * in the case of {@link KIND_SPECIAL}, one of the SPECIAL_* constants).
158: * @param e an element
159: * @return a two-element string (kind and details), or null if this element is anomalous
160: */
161: static final ElementType typeOf(Element e) {
162: String name = e.getNodeName();
163: Node p = e.getParentNode();
164: if (p == null) {
165: throw new IllegalArgumentException("Detached node: " + e); // NOI18N
166: }
167: if (p.getNodeType() == Node.DOCUMENT_NODE) {
168: if (name.equals("project")) { // NOI18N
169: return new ElementType(Kind.PROJECT, null);
170: } else {
171: // Weird root element? Ignore.
172: return null;
173: }
174: } else if (p.getNodeType() == Node.ELEMENT_NODE) {
175: // Find ourselves in context.
176: ElementType ptype = typeOf((Element) p);
177: if (ptype == null) {
178: // Unknown parent, therefore this is unknown too.
179: return null;
180: }
181: switch (ptype.kind) {
182: case PROJECT:
183: // <project> may have <description>, or types, or targets, or tasks
184: if (name.equals("description")) { // NOI18N
185: return new ElementType(Kind.DESCRIPTION, null);
186: } else if (name.equals("target")) { // NOI18N
187: return new ElementType(Kind.TARGET, null);
188: } else if (name.equals("import")) { // NOI18N
189: return new ElementType(Kind.IMPORT, null);
190: } else {
191: String taskClazz = getAntGrammar().getDefs("task")
192: .get(name); // NOI18N
193: if (taskClazz != null) {
194: return new ElementType(Kind.TASK, taskClazz);
195: } else {
196: String typeClazz = getAntGrammar().getDefs(
197: "type").get(name); // NOI18N
198: if (typeClazz != null) {
199: return new ElementType(Kind.TYPE, typeClazz);
200: } else {
201: return null;
202: }
203: }
204: }
205: case TARGET:
206: // <target> may have tasks and types
207: String taskClazz = getAntGrammar().getDefs("task").get(
208: name); // NOI18N
209: if (taskClazz != null) {
210: return new ElementType(Kind.TASK, taskClazz);
211: } else {
212: String typeClazz = getAntGrammar().getDefs("type")
213: .get(name); // NOI18N
214: if (typeClazz != null) {
215: return new ElementType(Kind.TYPE, typeClazz);
216: } else {
217: return null;
218: }
219: }
220: case DESCRIPTION:
221: // <description> should have no children!
222: return null;
223: case IMPORT:
224: // <import> should have no children!
225: return null;
226: default:
227: // We must be data.
228: String clazz = getAntGrammar().getElements(ptype.name)
229: .get(name);
230: if (clazz != null) {
231: return new ElementType(Kind.DATA, clazz);
232: } else {
233: // Unknown data.
234: return null;
235: }
236: }
237: } else {
238: throw new IllegalArgumentException("Bad parent for "
239: + e.toString() + ": " + p); // NOI18N
240: }
241: }
242:
243: /**
244: * @stereotype query
245: * @output list of results that can be queried on name, and attributes
246: * @time Performs fast up to 300 ms.
247: * @param ctx represents virtual attribute <code>Node</code> to be replaced. Its parent is a element node.
248: * @return list of <code>CompletionResult</code>s (ATTRIBUTE_NODEs) that can be queried on name, and attributes.
249: * Every list member represents one possibility.
250: */
251: public Enumeration<GrammarResult> queryAttributes(HintContext ctx) {
252:
253: Element ownerElement = null;
254: // Support both versions of GrammarQuery contract
255: if (ctx.getNodeType() == Node.ATTRIBUTE_NODE) {
256: ownerElement = ((Attr) ctx).getOwnerElement();
257: } else if (ctx.getNodeType() == Node.ELEMENT_NODE) {
258: ownerElement = (Element) ctx;
259: }
260: if (ownerElement == null) {
261: return Enumerations.empty();
262: }
263:
264: NamedNodeMap existingAttributes = ownerElement.getAttributes();
265: List<String> possibleAttributes;
266: ElementType type = typeOf(ownerElement);
267: if (type == null) {
268: return Enumerations.empty();
269: }
270:
271: switch (type.kind) {
272: case PROJECT:
273: possibleAttributes = new LinkedList<String>();
274: possibleAttributes.add("default");
275: possibleAttributes.add("name");
276: possibleAttributes.add("basedir");
277: break;
278: case TARGET:
279: possibleAttributes = new LinkedList<String>();
280: possibleAttributes.add("name");
281: possibleAttributes.add("depends");
282: possibleAttributes.add("description");
283: possibleAttributes.add("if");
284: possibleAttributes.add("unless");
285: break;
286: case DESCRIPTION:
287: return Enumerations.empty();
288: case IMPORT:
289: possibleAttributes = new LinkedList<String>();
290: possibleAttributes.add("file");
291: possibleAttributes.add("optional");
292: break;
293: default:
294: // task, type, or data; anyway, we have the defining class
295: possibleAttributes = new LinkedList<String>();
296: if (type.kind == Kind.TYPE) {
297: possibleAttributes.add("id");
298: }
299: if (getAntGrammar().isKnown(type.name)) {
300: possibleAttributes.addAll(new TreeSet<String>(
301: getAntGrammar().getAttributes(type.name)
302: .keySet()));
303: }
304: if (type.kind == Kind.TASK) {
305: // Can have an ID too, but less important; leave at end.
306: possibleAttributes.add("id");
307: // Currently IntrospectedInfo includes this in the props for a type,
308: // though it excludes it for tasks. So for now add it explicitly
309: // only to tasks.
310: possibleAttributes.add("description");
311: // Also useful sometimes:
312: possibleAttributes.add("taskname");
313: }
314: }
315:
316: String prefix = ctx.getCurrentPrefix();
317:
318: List<GrammarResult> list = new ArrayList<GrammarResult>();
319: for (String attribute : possibleAttributes) {
320: if (attribute.startsWith(prefix)) {
321: if (existingAttributes.getNamedItem(attribute) == null) {
322: list.add(new MyAttr(attribute));
323: }
324: }
325: }
326:
327: LOG.log(Level.FINE, "queryAttributes({0}) -> {1}",
328: new Object[] { prefix, list });
329: return Collections.enumeration(list);
330: }
331:
332: /**
333: * @semantics Navigates through read-only Node tree to determine context and provide right results.
334: * @postconditions Let ctx unchanged
335: * @time Performs fast up to 300 ms.
336: * @stereotype query
337: * @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.
338: * @return list of <code>CompletionResult</code>s (ELEMENT_NODEs) that can be queried on name, and attributes
339: * Every list member represents one possibility.
340: */
341: public Enumeration<GrammarResult> queryElements(HintContext ctx) {
342:
343: Node parent = ((Node) ctx).getParentNode();
344: if (parent == null) {
345: return Enumerations.empty();
346: }
347: if (parent.getNodeType() != Node.ELEMENT_NODE) {
348: return Enumerations.empty();
349: }
350:
351: List<String> elements;
352: ElementType type = typeOf((Element) parent);
353: if (type == null) {
354: return Enumerations.empty();
355: }
356:
357: switch (type.kind) {
358: case PROJECT:
359: elements = new LinkedList<String>();
360: elements.add("target");
361: elements.add("import");
362: elements.add("property");
363: elements.add("description");
364: SortedSet<String> tasks = getSortedDefs("task");
365: tasks.remove("property");
366: tasks.remove("import");
367: elements.addAll(tasks); // Ant 1.6 permits any tasks at top level
368: elements.addAll(getSortedDefs("type"));
369: break;
370: case TARGET:
371: elements = new ArrayList<String>(getSortedDefs("task"));
372: // targets can have embedded types too, though less common:
373: elements.addAll(getSortedDefs("type")); // NOI18N
374: break;
375: case DESCRIPTION:
376: return Enumerations.empty();
377: case IMPORT:
378: return Enumerations.empty();
379: default:
380: // some introspectable class
381: if (getAntGrammar().isKnown(type.name)) {
382: elements = new ArrayList<String>(
383: new TreeSet<String>(getAntGrammar()
384: .getElements(type.name).keySet()));
385: } else {
386: elements = Collections.emptyList();
387: }
388: }
389:
390: String prefix = ctx.getCurrentPrefix();
391:
392: List<GrammarResult> list = new ArrayList<GrammarResult>();
393: for (String element : elements) {
394: if (element.startsWith(prefix)) {
395: list.add(new MyElement(element));
396: }
397: }
398:
399: LOG.log(Level.FINE, "queryElements({0}) -> {1}", new Object[] {
400: prefix, list });
401: return Collections.enumeration(list);
402: }
403:
404: private static SortedSet<String> getSortedDefs(String kind) {
405: SortedSet<String> defs = new TreeSet<String>(Collator
406: .getInstance());
407: defs.addAll(getAntGrammar().getDefs(kind).keySet());
408: return defs;
409: }
410:
411: /**
412: * Allow to get names of <b>declared notations</b>.
413: * @return list of <code>CompletionResult</code>s (NOTATION_NODEs)
414: */
415: public Enumeration<GrammarResult> queryNotations(String prefix) {
416: return Enumerations.empty();
417: }
418:
419: public Enumeration<GrammarResult> queryValues(HintContext ctx) {
420: LOG.log(Level.FINE, "queryValues({0})", ctx.getCurrentPrefix());
421: // #38341: ctx is apparently instanceof Attr or Text
422: // (actually never instanceof Text, just TEXT_NODE: #38339)
423: Attr ownerAttr;
424: if (canCompleteProperty(ctx.getCurrentPrefix())) {
425: LOG.fine("...can complete property");
426: return completeProperties(ctx);
427: } else if (ctx.getNodeType() == Node.ATTRIBUTE_NODE) {
428: ownerAttr = (Attr) ctx;
429: } else {
430: LOG.fine("...unknown node type");
431: return Enumerations.empty();
432: }
433: Element ownerElement = ownerAttr.getOwnerElement();
434: String attrName = ownerAttr.getName();
435: ElementType type = typeOf(ownerElement);
436: if (type == null) {
437: LOG.fine("...unknown type");
438: return Enumerations.empty();
439: }
440: List<String> choices = new ArrayList<String>();
441:
442: switch (type.kind) {
443: case PROJECT:
444: if (attrName.equals("default")) {
445: // XXX list known targets?
446: } else if (attrName.equals("basedir")) {
447: // XXX file completion?
448: }
449: // freeform: name
450: break;
451: case TARGET:
452: if (attrName.equals("depends")) {
453: // XXX list known targets?
454: } else if (attrName.equals("if")
455: || attrName.equals("unless")) {
456: choices.addAll(Arrays.asList(likelyPropertyNames(ctx)));
457: }
458: // freeform: description
459: break;
460: case DESCRIPTION:
461: // nothing applicable
462: break;
463: case IMPORT:
464: if (attrName.equals("file")) {
465: // freeform
466: } else if (attrName.equals("optional")) {
467: choices.add("true");
468: choices.add("false");
469: }
470: break;
471: default:
472: String elementClazz = type.name;
473: if (getAntGrammar().isKnown(elementClazz)) {
474: String attrClazzName = getAntGrammar().getAttributes(
475: elementClazz).get(attrName);
476: if (attrClazzName != null) {
477: if (getAntGrammar().isKnown(attrClazzName)) {
478: String[] enumTags = getAntGrammar().getTags(
479: attrClazzName);
480: if (enumTags != null) {
481: choices.addAll(Arrays.asList(enumTags));
482: }
483: }
484: if (attrClazzName.equals("boolean")) {
485: choices.add("true");
486: choices.add("false");
487: } else if (attrClazzName
488: .equals("org.apache.tools.ant.types.Reference")) {
489: // XXX add names of ids
490: } else if (attrClazzName
491: .equals("org.apache.tools.ant.types.Path")
492: || attrClazzName.equals("java.io.File")
493: /* || "path" attr on Path or Path.Element */
494: ) {
495: // XXX complete filenames
496: } else if (attrClazzName.equals("java.lang.String")
497: && Arrays
498: .asList(
499: PROPERTY_NAME_VALUED_PROPERTY_NAMES)
500: .contains(attrName)) {
501: // <isset property="..."/>, <include name="*" unless="..."/>, etc.
502: choices.addAll(Arrays
503: .asList(likelyPropertyNames(ctx)));
504: }
505: }
506: }
507: }
508:
509: // Create the completion:
510: String prefix = ctx.getCurrentPrefix();
511: List<GrammarResult> list = new ArrayList<GrammarResult>();
512: for (String choice : choices) {
513: if (choice.startsWith(prefix)) {
514: list.add(new MyText(choice));
515: }
516: }
517:
518: LOG.log(Level.FINE, "queryValues({0}) -> {1}", new Object[] {
519: prefix, list });
520: return Collections.enumeration(list);
521: }
522:
523: /**
524: * Check whether a given content string (of an attribute value or of an element's
525: * content) has an uncompleted "${" sequence in it, i.e. one that has not been matched
526: * with a corresponding "}".
527: * E.g.:
528: * <pathelement location="${foo
529: * ^ caret
530: * Also if the last character is "$" it can be completed.
531: * @param content the current content of the attribute value or element
532: * @return true if there is an uncompleted property here
533: */
534: private static boolean canCompleteProperty(String content) {
535: content = deletedEscapedShells(content);
536: if (content.length() == 0) {
537: return false;
538: }
539: if (content.charAt(content.length() - 1) == '$') {
540: return true;
541: }
542: int idx = content.lastIndexOf("${");
543: return idx != -1 && content.indexOf('}', idx) == -1;
544: }
545:
546: private static Enumeration<GrammarResult> completeProperties(
547: HintContext ctx) {
548: String content = ctx.getCurrentPrefix();
549: assert content.length() > 0;
550: String header;
551: String propPrefix;
552: if (content.charAt(content.length() - 1) == '$') {
553: header = content + '{';
554: propPrefix = "";
555: } else {
556: int idx = content.lastIndexOf("${");
557: assert idx != -1;
558: header = content.substring(0, idx + 2);
559: propPrefix = content.substring(idx + 2);
560: }
561: String[] props = likelyPropertyNames(ctx);
562: // completion on text works differently from attrs:
563: // the context should not be returned (#38342)
564: boolean shortHeader = ctx.getNodeType() == Node.TEXT_NODE;
565: List<GrammarResult> list = new ArrayList<GrammarResult>();
566: for (int i = 0; i < props.length; i++) {
567: if (props[i].startsWith(propPrefix)) {
568: String text = header + props[i] + '}';
569: if (shortHeader) {
570: assert text.startsWith(content) : "text=" + text
571: + " content=" + content;
572: text = text.substring(content.length());
573: }
574: list.add(new MyText(text));
575: }
576: }
577: LOG.log(Level.FINE, "completeProperties({0}) -> {1}",
578: new Object[] { content, list });
579: return Collections.enumeration(list);
580: }
581:
582: /**
583: * Names of Ant properties that are generally present and defined in any script.
584: */
585: private static final String[] STOCK_PROPERTY_NAMES = {
586: // Present in most Ant installations:
587: "ant.home", // NOI18N
588: // Defined by Ant as standard properties:
589: "basedir", // NOI18N
590: "ant.file", // NOI18N
591: "ant.project.name", // NOI18N
592: "ant.java.version", // NOI18N
593: "ant.version", // NOI18N
594: // Defined by System.getProperties as standard system properties:
595: "java.version", // NOI18N
596: "java.vendor", // NOI18N
597: "java.vendor.url", // NOI18N
598: "java.home", // NOI18N
599: "java.vm.specification.version", // NOI18N
600: "java.vm.specification.vendor", // NOI18N
601: "java.vm.specification.name", // NOI18N
602: "java.vm.version", // NOI18N
603: "java.vm.vendor", // NOI18N
604: "java.vm.name", // NOI18N
605: "java.specification.version", // NOI18N
606: "java.specification.vendor", // NOI18N
607: "java.specification.name", // NOI18N
608: "java.class.version", // NOI18N
609: "java.class.path", // NOI18N
610: "java.library.path", // NOI18N
611: "java.io.tmpdir", // NOI18N
612: "java.compiler", // NOI18N
613: "java.ext.dirs", // NOI18N
614: "os.name", // NOI18N
615: "os.arch", // NOI18N
616: "os.version", // NOI18N
617: "file.separator", // NOI18N
618: "path.separator", // NOI18N
619: "line.separator", // NOI18N
620: "user.name", // NOI18N
621: "user.home", // NOI18N
622: "user.dir", // NOI18N
623: };
624:
625: private static String[] likelyPropertyNames(HintContext ctx) {
626: // #38343: ctx.getOwnerDocument returns some bogus unusable empty thing
627: // so find the root element manually
628: Element parent;
629: // #38341: docs for queryValues says Attr or Element, but really Attr or Text
630: // (and CDataSection never seems to permit completion at all...)
631: if (ctx.getNodeType() == Node.ATTRIBUTE_NODE) {
632: parent = ((Attr) ctx).getOwnerElement();
633: } else if (ctx.getNodeType() == Node.TEXT_NODE) {
634: Node p = ctx.getParentNode();
635: if (p != null && p.getNodeType() == Node.ELEMENT_NODE) {
636: parent = (Element) p;
637: } else {
638: System.err.println("strange parent of text node: "
639: + p.getNodeType() + " " + p);
640: return new String[0];
641: }
642: } else {
643: System.err.println("strange context type: "
644: + ctx.getNodeType() + " " + ctx);
645: return new String[0];
646: }
647: while (parent.getParentNode() != null
648: && parent.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
649: parent = (Element) parent.getParentNode();
650: }
651: // #38343: getElementsByTagName just throws an exception, you can't use it...
652: Set<String> choices = new TreeSet<String>(Arrays
653: .asList(STOCK_PROPERTY_NAMES));
654: visitForLikelyPropertyNames(parent, choices);
655: Iterator<String> it = choices.iterator();
656: while (it.hasNext()) {
657: String propname = it.next();
658: if (propname.indexOf("${") != -1) {
659: // Not actually a direct property name, rather a computed name.
660: // Skip it as it cannot be used here.
661: it.remove();
662: }
663: }
664: return choices.toArray(new String[choices.size()]);
665: }
666:
667: private static final String[] PROPERTY_NAME_VALUED_PROPERTY_NAMES = {
668: "if", "unless",
669: // XXX accept any *property
670: "property", "failureproperty", "errorproperty",
671: "addproperty", };
672:
673: private static void visitForLikelyPropertyNames(Node n,
674: Set<String> choices) {
675: int type = n.getNodeType();
676: switch (type) {
677: case Node.ELEMENT_NODE:
678: // XXX would be more precise to use typeOf here, but maybe slower?
679: // Look for <property name="propname" .../> and similar
680: Element el = (Element) n;
681: String tagname = el.getTagName();
682: if (tagname.equals("property")) {
683: String propname = el.getAttribute("name");
684: // #38343: Element impl is broken and can return null from getAttribute
685: if (propname != null && propname.length() > 0) {
686: choices.add(propname);
687: }
688: // XXX handle <property file="..."/> with a resolvable filename
689: } else if (tagname.equals("buildnumber")) {
690: // This task always defines ${build.number}
691: choices.add("build.number");
692: } else if (tagname.equals("tstamp")) {
693: // XXX handle prefix="whatever" -> ${whatever.TODAY} etc.
694: // XXX handle nested <format property="foo" .../> -> ${foo}
695: choices.add("DSTAMP");
696: choices.add("TSTAMP");
697: choices.add("TODAY");
698: }
699: // <available>, <dirname>, <pathconvert>, <uptodate>, <target>, <isset>, <include>, etc.
700: for (int i = 0; i < PROPERTY_NAME_VALUED_PROPERTY_NAMES.length; i++) {
701: String propname = el
702: .getAttribute(PROPERTY_NAME_VALUED_PROPERTY_NAMES[i]);
703: if (propname != null && propname.length() > 0) {
704: choices.add(propname);
705: }
706: }
707: break;
708: case Node.ATTRIBUTE_NODE:
709: case Node.TEXT_NODE:
710: // Look for ${propname}
711: String text = deletedEscapedShells(n.getNodeValue());
712: int idx = 0;
713: while (true) {
714: int start = text.indexOf("${", idx);
715: if (start == -1) {
716: break;
717: }
718: int end = text.indexOf('}', start + 2);
719: if (end == -1) {
720: break;
721: }
722: String propname = text.substring(start + 2, end);
723: if (propname.length() > 0) {
724: choices.add(propname);
725: }
726: idx = end + 1;
727: }
728: break;
729: default:
730: // ignore
731: break;
732: }
733: NodeList l = n.getChildNodes();
734: for (int i = 0; i < l.getLength(); i++) {
735: visitForLikelyPropertyNames(l.item(i), choices);
736: }
737: // Element attributes are not considered child nodes as such.
738: NamedNodeMap m = n.getAttributes();
739: if (m != null) {
740: for (int i = 0; i < m.getLength(); i++) {
741: visitForLikelyPropertyNames(m.item(i), choices);
742: }
743: }
744: }
745:
746: /**
747: * Remove pairs of '$$' to avoid being confused by them.
748: * They do not introduce property references.
749: */
750: private static String deletedEscapedShells(String text) {
751: // XXX could be faster w/o regexps
752: return text.replaceAll("\\$\\$", "");
753: }
754:
755: // return defaults, no way to query them
756: public GrammarResult queryDefault(final HintContext ctx) {
757: return null;
758: }
759:
760: // it is not yet implemented
761: public boolean isAllowed(Enumeration<GrammarResult> en) {
762: return true;
763: }
764:
765: // customizers section ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
766:
767: public java.awt.Component getCustomizer(HintContext ctx) {
768: return null;
769: }
770:
771: public boolean hasCustomizer(HintContext ctx) {
772: return false;
773: }
774:
775: public org.openide.nodes.Node.Property[] getProperties(
776: HintContext ctx) {
777: return null;
778: }
779:
780: // Result classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
781:
782: private static abstract class AbstractResultNode extends
783: AbstractNode implements GrammarResult {
784:
785: public Icon getIcon(int kind) {
786: return null;
787: }
788:
789: public String getDescription() {
790: return null;
791: }
792:
793: public String getDisplayName() {
794: return null;
795: }
796:
797: // TODO in MyElement return true for really empty elements such as "pathelement"
798: public boolean isEmptyElement() {
799: return false;
800: }
801: }
802:
803: private static class MyEntityReference extends AbstractResultNode
804: implements EntityReference {
805:
806: private String name;
807:
808: MyEntityReference(String name) {
809: this .name = name;
810: }
811:
812: public short getNodeType() {
813: return Node.ENTITY_REFERENCE_NODE;
814: }
815:
816: public @Override
817: String getNodeName() {
818: return name;
819: }
820:
821: }
822:
823: private static class MyElement extends AbstractResultNode implements
824: Element {
825:
826: private String name;
827:
828: MyElement(String name) {
829: this .name = name;
830: }
831:
832: public short getNodeType() {
833: return Node.ELEMENT_NODE;
834: }
835:
836: public @Override
837: String getNodeName() {
838: return name;
839: }
840:
841: public @Override
842: String getTagName() {
843: return name;
844: }
845:
846: public @Override
847: String toString() {
848: return name;
849: }
850:
851: }
852:
853: private static class MyAttr extends AbstractResultNode implements
854: Attr {
855:
856: private String name;
857:
858: MyAttr(String name) {
859: this .name = name;
860: }
861:
862: public short getNodeType() {
863: return Node.ATTRIBUTE_NODE;
864: }
865:
866: public @Override
867: String getNodeName() {
868: return name;
869: }
870:
871: public @Override
872: String getName() {
873: return name;
874: }
875:
876: public @Override
877: String getValue() {
878: return null; //??? what spec says
879: }
880:
881: public @Override
882: String toString() {
883: return name;
884: }
885:
886: }
887:
888: private static class MyText extends AbstractResultNode implements
889: Text {
890:
891: private String data;
892:
893: MyText(String data) {
894: this .data = data;
895: }
896:
897: public short getNodeType() {
898: return Node.TEXT_NODE;
899: }
900:
901: public @Override
902: String getNodeValue() {
903: return data;
904: }
905:
906: public @Override
907: String getData() throws DOMException {
908: return data;
909: }
910:
911: public @Override
912: int getLength() {
913: return data == null ? -1 : data.length();
914: }
915:
916: public @Override
917: String toString() {
918: return data;
919: }
920:
921: public @Override
922: String getDisplayName() {
923: return data; // #113804
924: }
925:
926: }
927:
928: }
|