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.apache.tools.ant.module.api;
043:
044: import java.io.IOException;
045: import java.io.InputStream;
046: import java.util.Arrays;
047: import java.util.Collections;
048: import java.util.Enumeration;
049: import java.util.HashSet;
050: import java.util.Map;
051: import java.util.Properties;
052: import java.util.Set;
053: import java.util.TreeMap;
054: import java.util.logging.Level;
055: import java.util.logging.Logger;
056: import java.util.prefs.BackingStoreException;
057: import java.util.prefs.Preferences;
058: import java.util.regex.Matcher;
059: import java.util.regex.Pattern;
060: import javax.swing.event.ChangeEvent;
061: import javax.swing.event.ChangeListener;
062: import org.apache.tools.ant.module.AntModule;
063: import org.apache.tools.ant.module.AntSettings;
064: import org.apache.tools.ant.module.bridge.AntBridge;
065: import org.apache.tools.ant.module.bridge.IntrospectionHelperProxy;
066: import org.openide.ErrorManager;
067: import org.openide.util.ChangeSupport;
068: import org.openide.util.NbCollections;
069: import org.openide.util.RequestProcessor;
070: import org.openide.util.Utilities;
071: import org.openide.util.WeakListeners;
072:
073: // XXX in order to support Ant 1.6 interface addition types, need to keep
074: // track of which classes implement a given interface
075:
076: /** Represents Ant-style introspection info for a set of classes.
077: * There should be one instance which is loaded automatically
078: * from defaults.properties files, i.e. standard tasks/datatypes.
079: * A second is loaded from settings and represents custom tasks/datatypes.
080: * Uses Ant's IntrospectionHelper for the actual work, but manages the results
081: * and makes them safely serializable (stores only classnames, etc.).
082: * <p>
083: * All task and type names may be namespace-qualified for use
084: * in Ant 1.6: a name of the form <samp>nsuri:localname</samp> refers to
085: * an XML element with namespace <samp>nsuri</samp> and local name <samp>localname</samp>.
086: * Attribute names could also be similarly qualified, but in practice attributes
087: * used in Ant never have a defined namespace. The prefix <samp>antlib:org.apache.tools.ant:</samp>
088: * is implied, not expressed, on Ant core element names (for backwards compatibility).
089: * Subelement names are *not* namespace-qualified here, even though in the script
090: * they would be - because the namespace used in the script will actually vary
091: * according to how an antlib is imported and used. An unqualified subelement name
092: * should be understood to inherit a namespace from its parent element.
093: * <em>(Namespace support since <code>org.apache.tools.ant.module/3 3.6</code>)</em>
094: */
095: public final class IntrospectedInfo {
096:
097: private static final Logger LOG = Logger
098: .getLogger(IntrospectedInfo.class.getName());
099:
100: private static IntrospectedInfo defaults = null;
101: private static boolean defaultsInited = false;
102: private static boolean defaultsEverInited = false;
103:
104: /** Get default definitions specified by Ant's defaults.properties.
105: * @return the singleton defaults
106: */
107: public static synchronized IntrospectedInfo getDefaults() {
108: if (defaults == null) {
109: defaults = new IntrospectedInfo();
110: }
111: return defaults;
112: }
113:
114: private Map<String, IntrospectedClass> clazzes = Collections
115: .synchronizedMap(new TreeMap<String, IntrospectedClass>());
116: /** definitions first by kind then by name to class name */
117: private Map<String, Map<String, String>> namedefs = new TreeMap<String, Map<String, String>>();
118:
119: private final ChangeSupport cs = new ChangeSupport(this );
120:
121: private ChangeListener antBridgeListener = new ChangeListener() {
122: public void stateChanged(ChangeEvent ev) {
123: clearDefs();
124: fireStateChanged();
125: }
126: };
127:
128: /** Make new empty set of info.
129: */
130: public IntrospectedInfo() {
131: }
132:
133: private void init() {
134: synchronized (IntrospectedInfo.class) {
135: if (!defaultsInited && this == defaults) {
136: AntModule.err
137: .log("IntrospectedInfo.getDefaults: loading...");
138: defaultsInited = true;
139: loadDefaults(!defaultsEverInited);
140: defaultsEverInited = true;
141: }
142: }
143: }
144:
145: private void clearDefs() {
146: clazzes.clear();
147: namedefs.clear();
148: defaultsInited = false;
149: }
150:
151: private void loadDefaults(boolean listen) {
152: ClassLoader cl = AntBridge.getMainClassLoader();
153: InputStream taskDefaults = cl
154: .getResourceAsStream("org/apache/tools/ant/taskdefs/defaults.properties");
155: if (taskDefaults != null) {
156: try {
157: defaults.load(taskDefaults, "task", cl); // NOI18N
158: } catch (IOException ioe) {
159: AntModule.err.log("Could not load default taskdefs");
160: AntModule.err.notify(ioe);
161: }
162: } else {
163: AntModule.err.log("Could not open default taskdefs");
164: }
165: InputStream typeDefaults = cl
166: .getResourceAsStream("org/apache/tools/ant/types/defaults.properties");
167: if (typeDefaults != null) {
168: try {
169: defaults.load(typeDefaults, "type", cl); // NOI18N
170: } catch (IOException ioe) {
171: AntModule.err.log("Could not load default typedefs");
172: AntModule.err.notify(ioe);
173: }
174: } else {
175: AntModule.err.log("Could not open default typedefs");
176: }
177: defaults.loadNetBeansSpecificDefinitions();
178: if (listen) {
179: AntBridge.addChangeListener(WeakListeners.change(
180: antBridgeListener, AntBridge.class));
181: }
182: if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
183: AntModule.err.log("IntrospectedInfo.defaults=" + defaults);
184: }
185: }
186:
187: /** Add a listener to changes in the definition set.
188: * @param l the listener to add
189: * @since 2.6
190: */
191: public void addChangeListener(ChangeListener l) {
192: cs.addChangeListener(l);
193: }
194:
195: /** Remove a listener to changes in the definition set.
196: * @param l the listener to remove
197: * @since 2.6
198: */
199: public void removeChangeListener(ChangeListener l) {
200: cs.removeChangeListener(l);
201: }
202:
203: private class ChangeTask implements Runnable {
204: public void run() {
205: cs.fireChange();
206: }
207: }
208:
209: private void fireStateChanged() {
210: if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
211: AntModule.err.log("IntrospectedInfo.fireStateChanged");
212: }
213: RequestProcessor.getDefault().post(new ChangeTask());
214: }
215:
216: /** Get definitions.
217: * @param kind the kind of definition, e.g. <code>task</code>
218: * @return an immutable map from definition names to class names
219: */
220: public Map<String, String> getDefs(String kind) {
221: init();
222: synchronized (namedefs) {
223: Map<String, String> m = namedefs.get(kind);
224: if (m != null) {
225: return Collections.unmodifiableMap(m);
226: } else {
227: return Collections.emptyMap();
228: }
229: }
230: }
231:
232: private IntrospectedClass getData(String clazz)
233: throws IllegalArgumentException {
234: IntrospectedClass data = clazzes.get(clazz);
235: if (data == null) {
236: throw new IllegalArgumentException("Unknown class: "
237: + clazz); // NOI18N
238: }
239: return data;
240: }
241:
242: /** Is anything known about this class?
243: * @param clazz the class name
244: * @return true if it is known, false if never encountered
245: */
246: public boolean isKnown(String clazz) {
247: init();
248: return clazzes.get(clazz) != null;
249: }
250:
251: /** Does this class support inserting text data?
252: * @param clazz the class name
253: * @return true if so
254: * @throws IllegalArgumentException if the class is unknown
255: */
256: public boolean supportsText(String clazz)
257: throws IllegalArgumentException {
258: init();
259: return getData(clazz).supportsText;
260: }
261:
262: /** Get all attributes supported by this class.
263: * @param clazz the class name
264: * @return an immutable map from attribute name to type (class name)
265: * @throws IllegalArgumentException if the class is unknown
266: */
267: public Map<String, String> getAttributes(String clazz)
268: throws IllegalArgumentException {
269: init();
270: Map<String, String> map = getData(clazz).attrs;
271: if (map == null) {
272: return Collections.emptyMap();
273: } else {
274: return Collections.unmodifiableMap(map);
275: }
276: }
277:
278: /** Get all subelements supported by this class.
279: * @param clazz the class name
280: * @return an immutable map from element name to type (class name)
281: * @throws IllegalArgumentException if the class is unknown
282: */
283: public Map<String, String> getElements(String clazz)
284: throws IllegalArgumentException {
285: init();
286: Map<String, String> map = getData(clazz).subs;
287: if (map == null) {
288: return Collections.emptyMap();
289: } else {
290: return Collections.unmodifiableMap(map);
291: }
292: }
293:
294: /**
295: * Get tags represented by this class if it is an <code>EnumeratedAttribute</code>.
296: * @param clazz the class name
297: * @return a list of tag names, or null if the class is not a subclass of <code>EnumeratedAttribute</code>
298: * @throws IllegalArgumentException if the class is unknown
299: * @since org.apache.tools.ant.module/3 3.3
300: */
301: public String[] getTags(String clazz)
302: throws IllegalArgumentException {
303: init();
304: return getData(clazz).enumTags;
305: }
306:
307: /** Load defs from a properties file. */
308: private void load(InputStream is, String kind, ClassLoader cl)
309: throws IOException {
310: Properties p = new Properties();
311: try {
312: p.load(is);
313: } finally {
314: is.close();
315: }
316: for (Map.Entry<String, String> entry : NbCollections
317: .checkedMapByFilter(p, String.class, String.class, true)
318: .entrySet()) {
319: String name = entry.getKey();
320: if (kind.equals("type") && name.equals("description")) { // NOI18N
321: // Not a real data type; handled specially.
322: AntModule.err
323: .log("Skipping pseudodef of <description>");
324: continue;
325: }
326: String clazzname = entry.getValue();
327: try {
328: Class clazz = cl.loadClass(clazzname);
329: register(name, clazz, kind, false);
330: } catch (ClassNotFoundException cnfe) {
331: // This is normal, e.g. Ant's taskdefs include optional tasks we don't have.
332: AntModule.err.log("IntrospectedInfo: skipping "
333: + clazzname + ": " + cnfe);
334: } catch (NoClassDefFoundError ncdfe) {
335: // Normal for e.g. optional tasks which we cannot resolve against.
336: AntModule.err.log("IntrospectedInfo: skipping "
337: + clazzname + ": " + ncdfe);
338: } catch (LinkageError e) {
339: // Not normal; if it is there it ought to be resolvable etc.
340: throw (IOException) new IOException(
341: "Could not load class " + clazzname + ": " + e)
342: .initCause(e); // NOI18N
343: } catch (RuntimeException e) {
344: // SecurityException etc. Not normal.
345: throw (IOException) new IOException(
346: "Could not load class " + clazzname + ": " + e)
347: .initCause(e); // NOI18N
348: }
349: }
350: }
351:
352: private void loadNetBeansSpecificDefinitions() {
353: loadNetBeansSpecificDefinitions0(AntBridge
354: .getCustomDefsNoNamespace());
355: if (AntBridge.getInterface().isAnt16()) {
356: // Define both.
357: loadNetBeansSpecificDefinitions0(AntBridge
358: .getCustomDefsWithNamespace());
359: }
360: }
361:
362: private void loadNetBeansSpecificDefinitions0(
363: Map<String, Map<String, Class>> defsByKind) {
364: for (Map.Entry<String, Map<String, Class>> kindE : defsByKind
365: .entrySet()) {
366: for (Map.Entry<String, Class> defsE : kindE.getValue()
367: .entrySet()) {
368: register(defsE.getKey(), defsE.getValue(), kindE
369: .getKey());
370: }
371: }
372: }
373:
374: /** Register a new definition.
375: * May change the defined task/type for a given name, but
376: * will not redefine structure if classes are modified.
377: * Also any class definitions contained in the default map (if not this one)
378: * are just ignored; you should refer to the default map for info on them.
379: * Throws various errors if the class could not be resolved, e.g. NoClassDefFoundError.
380: * @param name name of the task or type as it appears in scripts
381: * @param clazz the implementing class
382: * @param kind the kind of definition to register (<code>task</code> or <code>type</code> currently)
383: * @since 2.4
384: */
385: public synchronized void register(String name, Class clazz,
386: String kind) {
387: register(name, clazz, kind, true);
388: }
389:
390: private void register(String name, Class clazz, String kind,
391: boolean fire) {
392: init();
393: synchronized (namedefs) {
394: Map<String, String> m = namedefs.get(kind);
395: if (m == null) {
396: m = new TreeMap<String, String>();
397: namedefs.put(kind, m);
398: }
399: m.put(name, clazz.getName());
400: }
401: boolean changed = analyze(clazz, null, false);
402: if (changed && fire) {
403: fireStateChanged();
404: }
405: }
406:
407: /** Unregister a definition.
408: * Removes it from the definition mapping, though structural
409: * information about the implementing class (and classes referenced
410: * by that class) will not be removed.
411: * If the definition was not registered before, does nothing.
412: * @param name the definition name
413: * @param kind the kind of definition (<code>task</code> etc.)
414: * @since 2.4
415: */
416: public synchronized void unregister(String name, String kind) {
417: init();
418: synchronized (namedefs) {
419: Map<String, String> m = namedefs.get(kind);
420: if (m != null) {
421: m.remove(name);
422: }
423: }
424: fireStateChanged();
425: }
426:
427: /**
428: * Analyze a particular class and other classes recursively.
429: * Will never try to redefine anything in the default IntrospectedInfo.
430: * For custom IntrospectedInfo's, will never try to redefine anything
431: * if skipReanalysis is null. If not null, will not redefine anything
432: * in that set - so start recursion by passing an empty set, if you wish
433: * to redefine anything you come across recursively that is not in the
434: * default IntrospectedInfo, without causing loops.
435: * Attribute classes are examined just in case they are EnumeratedAttribute
436: * subclasses; they are not checked for subelements etc.
437: * Does not itself fire changes - you should do this if the return value is true.
438: * @param clazz the class to look at
439: * @param skipReanalysis null to do not redefs, or a set of already redef'd classes
440: * @param isAttrType false for an element class, true for an attribute class
441: * @return true if something changed
442: */
443: private boolean analyze(Class clazz, Set<Class> skipReanalysis,
444: boolean isAttrType) {
445: String n = clazz.getName();
446: /*
447: if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
448: AntModule.err.log("IntrospectedInfo.analyze: " + n + " skipping=" + skipReanalysis + " attrType=" + isAttrType);
449: }
450: */
451: if (getDefaults().isKnown(n)) {
452: // Never try to redefine anything in the default IntrospectedInfo.
453: return false;
454: }
455: if ((skipReanalysis == null || !skipReanalysis.add(clazz))
456: && /* #23630 */isKnown(n)) {
457: // Either we are not redefining anything; or we are, but this class
458: // has already been in the list. Skip it. If we are continuing, make
459: // sure to add this class to the skip list so we do not loop.
460: return false;
461: }
462: //AntModule.err.log ("IntrospectedInfo.analyze: clazz=" + clazz.getName ());
463: //boolean dbg = (clazz == org.apache.tools.ant.taskdefs.Taskdef.class);
464: //if (! dbg && clazz.getName ().equals ("org.apache.tools.ant.taskdefs.Taskdef")) { // NOI18N
465: // AntModule.err.log ("Classloader mismatch: cl1=" + clazz.getClassLoader () + " cl2=" + org.apache.tools.ant.taskdefs.Taskdef.class.getClassLoader ());
466: //}
467: //if (dbg) AntModule.err.log ("Analyzing <taskdef> attrs...");
468: IntrospectedClass info = new IntrospectedClass();
469: if (isAttrType) {
470: String[] enumTags = AntBridge.getInterface()
471: .getEnumeratedValues(clazz);
472: if (enumTags != null) {
473: info.enumTags = enumTags;
474: return !info.equals(clazzes.put(clazz.getName(), info));
475: } else {
476: // Do not store attr clazzes unless they are interesting: EnumAttr.
477: return clazzes.remove(clazz.getName()) != null;
478: }
479: // That's all we do - no subelements etc.
480: }
481: IntrospectionHelperProxy helper = AntBridge.getInterface()
482: .getIntrospectionHelper(clazz);
483: info.supportsText = helper.supportsCharacters();
484: Enumeration<String> e = helper.getAttributes();
485: Set<Class> nueAttrTypeClazzes = new HashSet<Class>();
486: //if (dbg) AntModule.err.log ("Analyzing <taskdef> attrs...");
487: if (e.hasMoreElements()) {
488: while (e.hasMoreElements()) {
489: String name = e.nextElement();
490: //if (dbg) AntModule.err.log ("\tname=" + name);
491: try {
492: Class attrType = helper.getAttributeType(name);
493: String type = attrType.getName();
494: //if (dbg) AntModule.err.log ("\ttype=" + type);
495: if (hasSuperclass(clazz,
496: "org.apache.tools.ant.Task")
497: && // NOI18N
498: ((name.equals("location") && type
499: .equals("org.apache.tools.ant.Location"))
500: || // NOI18N
501: (name.equals("taskname") && type
502: .equals("java.lang.String")) || // NOI18N
503: (name.equals("description") && type
504: .equals("java.lang.String")))) { // NOI18N
505: // IntrospectionHelper is supposed to exclude such things, but I guess not.
506: // Or it excludes location & taskType.
507: // description may be OK to actually show on nodes, but since it is common
508: // to all tasks it should not be stored as such. Ditto taskname.
509: continue;
510: }
511: // XXX also handle subclasses of DataType and its standard attrs
512: // incl. creating nicely-named node props for description, refid, etc.
513: if (info.attrs == null) {
514: info.attrs = new TreeMap<String, String>();
515: }
516: info.attrs.put(name, type);
517: nueAttrTypeClazzes.add(attrType);
518: } catch (RuntimeException re) { // i.e. BuildException; but avoid loading this class
519: AntModule.err
520: .notify(ErrorManager.INFORMATIONAL, re);
521: }
522: }
523: } else {
524: info.attrs = null;
525: }
526: Set<Class> nueClazzes = new HashSet<Class>();
527: e = helper.getNestedElements();
528: //if (dbg) AntModule.err.log ("Analyzing <taskdef> subels...");
529: if (e.hasMoreElements()) {
530: while (e.hasMoreElements()) {
531: String name = e.nextElement();
532: //if (dbg) AntModule.err.log ("\tname=" + name);
533: try {
534: Class subclazz = helper.getElementType(name);
535: //if (dbg) AntModule.err.log ("\ttype=" + subclazz.getName ());
536: if (info.subs == null) {
537: info.subs = new TreeMap<String, String>();
538: }
539: info.subs.put(name, subclazz.getName());
540: nueClazzes.add(subclazz);
541: } catch (RuntimeException re) { // i.e. BuildException; but avoid loading this class
542: AntModule.err
543: .notify(ErrorManager.INFORMATIONAL, re);
544: }
545: }
546: } else {
547: info.subs = null;
548: }
549: boolean changed = !info.equals(clazzes.put(clazz.getName(),
550: info));
551: // And recursively analyze reachable classes for subelements...
552: // (usually these will already be known, and analyze will return at once)
553: for (Class nueClazz : nueClazzes) {
554: changed |= analyze(nueClazz, skipReanalysis, false);
555: }
556: for (Class nueClazz : nueAttrTypeClazzes) {
557: changed |= analyze(nueClazz, skipReanalysis, true);
558: }
559: return changed;
560: }
561:
562: private static boolean hasSuperclass(Class subclass,
563: String super class) {
564: for (Class c = subclass; c != null; c = c.getSuperclass()) {
565: if (c.getName().equals(super class)) {
566: return true;
567: }
568: }
569: return false;
570: }
571:
572: /**
573: * Scan an existing (already-run) project to see if it has any new tasks/types.
574: * Any new definitions found will automatically be added to the known list.
575: * This will try to change existing definitions in the custom set, i.e.
576: * if a task is defined to be implemented with a different class, or if a
577: * class changes structure.
578: * Will not try to define anything contained in the defaults list.
579: * @param defs map from kinds to maps from names to classes
580: */
581: public void scanProject(Map<String, Map<String, Class>> defs) {
582: init();
583: Set<Class> skipReanalysis = new HashSet<Class>();
584: boolean changed = false;
585: for (Map.Entry<String, Map<String, Class>> e : defs.entrySet()) {
586: changed |= scanMap(e.getValue(), e.getKey(), skipReanalysis);
587: }
588: if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
589: AntModule.err.log("IntrospectedInfo.scanProject: " + this );
590: }
591: if (changed) {
592: fireStateChanged();
593: }
594: }
595:
596: private boolean scanMap(Map<String, Class> m, String kind,
597: Set<Class> skipReanalysis) {
598: if (kind == null)
599: throw new IllegalArgumentException();
600: boolean changed = false;
601: for (Map.Entry<String, Class> entry : m.entrySet()) {
602: String name = entry.getKey();
603: if (kind.equals("type") && name.equals("description")) { // NOI18N
604: // Not a real data type; handled specially.
605: AntModule.err
606: .log("Skipping pseudodef of <description>");
607: continue;
608: }
609: Class clazz = entry.getValue();
610: if (clazz.getName().equals(
611: "org.apache.tools.ant.taskdefs.MacroInstance")) { // NOI18N
612: continue;
613: }
614: Map<String, String> registry = namedefs.get(kind);
615: if (registry == null) {
616: registry = new TreeMap<String, String>();
617: namedefs.put(kind, registry);
618: }
619: synchronized (this ) {
620: if (getDefaults().getDefs(kind).get(name) == null) {
621: changed |= !clazz.getName().equals(
622: registry.put(name, clazz.getName()));
623: }
624: if (!getDefaults().isKnown(clazz.getName())) {
625: try {
626: changed |= analyze(clazz, skipReanalysis, false);
627: } catch (ThreadDeath td) {
628: throw td;
629: } catch (NoClassDefFoundError ncdfe) {
630: // Reasonably normal.
631: AntModule.err.log("Skipping " + clazz.getName()
632: + ": " + ncdfe);
633: } catch (LinkageError e) {
634: // Not so normal.
635: AntModule.err.annotate(e,
636: ErrorManager.INFORMATIONAL,
637: "Cannot scan class " + clazz.getName(),
638: null, null, null); // NOI18N
639: AntModule.err.notify(
640: ErrorManager.INFORMATIONAL, e);
641: }
642: }
643: }
644: }
645: return changed;
646: }
647:
648: @Override
649: public String toString() {
650: return "IntrospectedInfo[namedefs=" + namedefs + ",clazzes="
651: + clazzes + "]"; // NOI18N
652: }
653:
654: private static final class IntrospectedClass {
655:
656: public boolean supportsText;
657: public Map<String, String> attrs; // null or name -> class
658: public Map<String, String> subs; // null or name -> class
659: public String[] enumTags; // null or list of tags
660:
661: @Override
662: public String toString() {
663: return "IntrospectedClass[text=" + supportsText + ",attrs="
664: + attrs + ",subs=" + subs + ",enumTags="
665: + Arrays.toString(enumTags) + "]"; // NOI18N
666: }
667:
668: @Override
669: public int hashCode() {
670: // XXX
671: return 0;
672: }
673:
674: @Override
675: public boolean equals(Object o) {
676: if (!(o instanceof IntrospectedClass)) {
677: return false;
678: }
679: IntrospectedClass other = (IntrospectedClass) o;
680: return supportsText == other.supportsText
681: && Utilities.compareObjects(attrs, other.attrs)
682: && Utilities.compareObjects(subs, other.subs)
683: && Utilities.compareObjects(enumTags,
684: other.enumTags);
685: }
686:
687: }
688:
689: // merging and including custom defs:
690:
691: /** only used to permit use of WeakListener */
692: private ChangeListener holder;
693:
694: /**
695: * Merge several IntrospectedInfo instances together.
696: * Responds live to updates.
697: */
698: private static IntrospectedInfo merge(IntrospectedInfo[] proxied) {
699: final IntrospectedInfo ii = new IntrospectedInfo();
700: ChangeListener l = new ChangeListener() {
701: public void stateChanged(ChangeEvent ev) {
702: IntrospectedInfo ii2 = (IntrospectedInfo) ev
703: .getSource();
704: ii2.init();
705: ii.clazzes.putAll(ii2.clazzes);
706: for (Map.Entry<String, Map<String, String>> e : ii2.namedefs
707: .entrySet()) {
708: String kind = e.getKey();
709: Map<String, String> entries = e.getValue();
710: if (ii.namedefs.containsKey(kind)) {
711: ii.namedefs.get(kind).putAll(entries);
712: } else {
713: ii.namedefs.put(kind,
714: new TreeMap<String, String>(entries));
715: }
716: }
717: ii.fireStateChanged();
718: }
719: };
720: ii.holder = l;
721: for (IntrospectedInfo info : proxied) {
722: info.addChangeListener(WeakListeners.change(l, info));
723: l.stateChanged(new ChangeEvent(info));
724: }
725: return ii;
726: }
727:
728: /** defaults + custom defs */
729: private static IntrospectedInfo merged;
730:
731: /**
732: * Get all known introspected definitions.
733: * Includes all those in {@link #getDefaults} plus custom definitions
734: * encountered in actual build scripts (details unspecified).
735: * @return a set of all known definitions, e.g. of tasks and types
736: * @since 2.14
737: */
738: public static synchronized IntrospectedInfo getKnownInfo() {
739: if (merged == null) {
740: merged = merge(new IntrospectedInfo[] { getDefaults(),
741: AntSettings.getCustomDefs(), });
742: }
743: return merged;
744: }
745:
746: static {
747: AntSettings.IntrospectedInfoSerializer.instance = new AntSettings.IntrospectedInfoSerializer() {
748: /*
749: Format quick key:
750: Map<String,Map<String,String>> namedefs: task.echo=org.apache.tools.ant.taskdefs.Echo
751: Map<String,IntrospectedClass> clazzes: class.org.apache.tools.ant.taskdefs.Echo.<...>
752: boolean supportsText: .supportsText=true
753: null | Map<String,String> attrs: .attrs.message=java.lang.String
754: null | Map<String,String> subs: .subs.file=java.io.File
755: null | String[] enumTags: .enumTags=whenempty,always,never
756: */
757: Pattern p = Pattern
758: .compile("(.+)\\.(supportsText|attrs\\.(.+)|subs\\.(.+)|enumTags)");
759:
760: public IntrospectedInfo load(Preferences node) {
761: IntrospectedInfo ii = new IntrospectedInfo();
762: try {
763: for (String k : node.keys()) {
764: String v = node.get(k, null);
765: assert v != null : k;
766: String[] ss = k.split("\\.", 2);
767: if (ss.length != 2) {
768: LOG.log(Level.WARNING,
769: "malformed key: {0}", k);
770: continue;
771: }
772: if (ss[0].equals("class")) {
773: Matcher m = p.matcher(ss[1]);
774: boolean match = m.matches();
775: if (!match) {
776: LOG.log(Level.WARNING,
777: "malformed key: {0}", k);
778: continue;
779: }
780: String c = m.group(1);
781: IntrospectedClass ic = assureDefined(ii, c);
782: String tail = m.group(2);
783: if (tail.equals("supportsText")) {
784: assert v.equals("true") : k;
785: ic.supportsText = true;
786: } else if (tail.equals("enumTags")) {
787: ic.enumTags = v.split(",");
788: } else if (m.group(3) != null) {
789: if (ic.attrs == null) {
790: ic.attrs = new TreeMap<String, String>();
791: }
792: ic.attrs.put(m.group(3), v);
793: //assureDefined(ii, v);
794: } else {
795: assert m.group(4) != null : k;
796: if (ic.subs == null) {
797: ic.subs = new TreeMap<String, String>();
798: }
799: ic.subs.put(m.group(4), v);
800: //assureDefined(ii, v);
801: }
802: } else {
803: Map<String, String> m = ii.namedefs
804: .get(ss[0]);
805: if (m == null) {
806: m = new TreeMap<String, String>();
807: ii.namedefs.put(ss[0], m);
808: }
809: m.put(ss[1], v);
810: //assureDefined(ii, v);
811: }
812: }
813: } catch (BackingStoreException x) {
814: LOG.log(Level.WARNING, null, x);
815: }
816: for (String kind : new String[] { "task", "type" }) {
817: if (!ii.namedefs.containsKey(kind)) {
818: ii.namedefs.put(kind,
819: new TreeMap<String, String>());
820: }
821: }
822: return ii;
823: }
824:
825: private IntrospectedClass assureDefined(
826: IntrospectedInfo ii, String clazz) {
827: IntrospectedClass ic = ii.clazzes.get(clazz);
828: if (ic == null) {
829: ic = new IntrospectedClass();
830: ii.clazzes.put(clazz, ic);
831: }
832: return ic;
833: }
834:
835: public void store(Preferences node, IntrospectedInfo info) {
836: try {
837: node.clear();
838: } catch (BackingStoreException x) {
839: LOG.log(Level.WARNING, null, x);
840: return;
841: }
842: for (Map.Entry<String, Map<String, String>> kindEntries : info.namedefs
843: .entrySet()) {
844: for (Map.Entry<String, String> namedef : kindEntries
845: .getValue().entrySet()) {
846: node.put(kindEntries.getKey() + "."
847: + namedef.getKey(), namedef.getValue());
848: }
849: }
850: for (Map.Entry<String, IntrospectedClass> clazzPair : info.clazzes
851: .entrySet()) {
852: String c = "class." + clazzPair.getKey();
853: IntrospectedClass ic = clazzPair.getValue();
854: if (ic.supportsText) {
855: node.putBoolean(c + ".supportsText", true);
856: }
857: if (ic.attrs != null) {
858: for (Map.Entry<String, String> attr : ic.attrs
859: .entrySet()) {
860: node.put(c + ".attrs." + attr.getKey(),
861: attr.getValue());
862: }
863: }
864: if (ic.subs != null) {
865: for (Map.Entry<String, String> sub : ic.subs
866: .entrySet()) {
867: node.put(c + ".subs." + sub.getKey(), sub
868: .getValue());
869: }
870: }
871: if (ic.enumTags != null) {
872: StringBuilder b = new StringBuilder();
873: for (String s : ic.enumTags) {
874: if (b.length() > 0) {
875: b.append(',');
876: }
877: b.append(s);
878: }
879: node.put(c + ".enumTags", b.toString());
880: }
881: }
882: // Exact equivalence is unlikely to happen; there may be unanalyzed Java classes, etc.
883: //assert equiv(info, load(node)) : info + " vs. " + load(node);
884: }
885:
886: private boolean equiv(IntrospectedInfo ii1,
887: IntrospectedInfo ii2) {
888: return ii1.clazzes.equals(ii2.clazzes)
889: && ii1.namedefs.equals(ii2.namedefs);
890: }
891: };
892: }
893:
894: }
|