blob: d6e236f77ee362c9fc8c340730eaff13a1eac9e3 [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.apache.tools.ant.module.api;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.tools.ant.module.AntModule;
import org.apache.tools.ant.module.AntSettings;
import org.apache.tools.ant.module.bridge.AntBridge;
import org.apache.tools.ant.module.bridge.IntrospectionHelperProxy;
import org.openide.ErrorManager;
import org.openide.util.ChangeSupport;
import org.openide.util.NbCollections;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
// XXX in order to support Ant 1.6 interface addition types, need to keep
// track of which classes implement a given interface
/** Represents Ant-style introspection info for a set of classes.
* There should be one instance which is loaded automatically
* from defaults.properties files, i.e. standard tasks/datatypes.
* A second is loaded from settings and represents custom tasks/datatypes.
* Uses Ant's IntrospectionHelper for the actual work, but manages the results
* and makes them safely serializable (stores only classnames, etc.).
* <p>
* All task and type names may be namespace-qualified for use
* in Ant 1.6: a name of the form <samp>nsuri:localname</samp> refers to
* an XML element with namespace <samp>nsuri</samp> and local name <samp>localname</samp>.
* Attribute names could also be similarly qualified, but in practice attributes
* used in Ant never have a defined namespace. The prefix <samp>antlib:org.apache.tools.ant:</samp>
* is implied, not expressed, on Ant core element names (for backwards compatibility).
* Subelement names are *not* namespace-qualified here, even though in the script
* they would be - because the namespace used in the script will actually vary
* according to how an antlib is imported and used. An unqualified subelement name
* should be understood to inherit a namespace from its parent element.
* <em>(Namespace support since <code>org.apache.tools.ant.module/3 3.6</code>)</em>
*/
public final class IntrospectedInfo {
private static final Logger LOG = Logger.getLogger(IntrospectedInfo.class.getName());
private static IntrospectedInfo defaults = null;
private static boolean defaultsInited = false;
private static boolean defaultsEverInited = false;
/** Get default definitions specified by Ant's defaults.properties.
* @return the singleton defaults
*/
public static synchronized IntrospectedInfo getDefaults() {
if (defaults == null) {
defaults = new IntrospectedInfo();
}
return defaults;
}
private Map<String,IntrospectedClass> clazzes = Collections.synchronizedMap(new TreeMap<String,IntrospectedClass>());
/** definitions first by kind then by name to class name */
private final Map<String,Map<String,String>> namedefs = new TreeMap<String,Map<String,String>>();
private final ChangeSupport cs = new ChangeSupport(this);
private ChangeListener antBridgeListener = new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
clearDefs();
fireStateChanged();
}
};
/** Make new empty set of info.
*/
public IntrospectedInfo () {
}
private void init() {
synchronized (IntrospectedInfo.class) {
if (!defaultsInited && this == defaults) {
AntModule.err.log("IntrospectedInfo.getDefaults: loading...");
defaultsInited = true;
loadDefaults(!defaultsEverInited);
defaultsEverInited = true;
}
}
}
private void clearDefs() {
clazzes.clear();
namedefs.clear();
defaultsInited = false;
}
private void loadDefaults(boolean listen) {
ClassLoader cl = AntBridge.getMainClassLoader();
InputStream taskDefaults = cl.getResourceAsStream("org/apache/tools/ant/taskdefs/defaults.properties");
if (taskDefaults != null) {
try {
defaults.load(taskDefaults, "task", cl); // NOI18N
} catch (IOException ioe) {
AntModule.err.log("Could not load default taskdefs");
AntModule.err.notify(ioe);
}
} else {
AntModule.err.log("Could not open default taskdefs");
}
InputStream typeDefaults = cl.getResourceAsStream("org/apache/tools/ant/types/defaults.properties");
if (typeDefaults != null) {
try {
defaults.load(typeDefaults, "type", cl); // NOI18N
} catch (IOException ioe) {
AntModule.err.log("Could not load default typedefs");
AntModule.err.notify(ioe);
}
} else {
AntModule.err.log("Could not open default typedefs");
}
defaults.loadNetBeansSpecificDefinitions();
if (listen) {
AntBridge.addChangeListener(WeakListeners.change(antBridgeListener, AntBridge.class));
}
if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
AntModule.err.log("IntrospectedInfo.defaults=" + defaults);
}
}
/** Add a listener to changes in the definition set.
* @param l the listener to add
* @since 2.6
*/
public void addChangeListener(ChangeListener l) {
cs.addChangeListener(l);
}
/** Remove a listener to changes in the definition set.
* @param l the listener to remove
* @since 2.6
*/
public void removeChangeListener(ChangeListener l) {
cs.removeChangeListener(l);
}
private class ChangeTask implements Runnable {
public void run() {
cs.fireChange();
}
}
private static final RequestProcessor RP = new RequestProcessor(IntrospectedInfo.class);
private void fireStateChanged() {
if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
AntModule.err.log("IntrospectedInfo.fireStateChanged");
}
RP.post(new ChangeTask());
}
/** Get definitions.
* @param kind the kind of definition, e.g. <code>task</code>
* @return an immutable map from definition names to class names
*/
public Map<String,String> getDefs(String kind) {
init();
synchronized (namedefs) {
Map<String,String> m = namedefs.get(kind);
if (m != null) {
return Collections.unmodifiableMap(m);
} else {
return Collections.emptyMap();
}
}
}
private IntrospectedClass getData (String clazz) throws IllegalArgumentException {
IntrospectedClass data = clazzes.get(clazz);
if (data == null) {
throw new IllegalArgumentException("Unknown class: " + clazz); // NOI18N
}
return data;
}
/** Is anything known about this class?
* @param clazz the class name
* @return true if it is known, false if never encountered
*/
public boolean isKnown (String clazz) {
init();
return clazzes.get (clazz) != null;
}
/** Does this class support inserting text data?
* @param clazz the class name
* @return true if so
* @throws IllegalArgumentException if the class is unknown
*/
public boolean supportsText (String clazz) throws IllegalArgumentException {
init();
return getData (clazz).supportsText;
}
/** Get all attributes supported by this class.
* @param clazz the class name
* @return an immutable map from attribute name to type (class name)
* @throws IllegalArgumentException if the class is unknown
*/
public Map<String,String> getAttributes(String clazz) throws IllegalArgumentException {
init();
Map<String,String> map = getData(clazz).attrs;
if (map == null) {
return Collections.emptyMap();
} else {
return Collections.unmodifiableMap (map);
}
}
/** Get all subelements supported by this class.
* @param clazz the class name
* @return an immutable map from element name to type (class name)
* @throws IllegalArgumentException if the class is unknown
*/
public Map<String,String> getElements(String clazz) throws IllegalArgumentException {
init();
Map<String,String> map = getData(clazz).subs;
if (map == null) {
return Collections.emptyMap();
} else {
return Collections.unmodifiableMap (map);
}
}
/**
* Get tags represented by this class if it is an <code>EnumeratedAttribute</code>.
* @param clazz the class name
* @return a list of tag names, or null if the class is not a subclass of <code>EnumeratedAttribute</code>
* @throws IllegalArgumentException if the class is unknown
* @since org.apache.tools.ant.module/3 3.3
*/
public String[] getTags(String clazz) throws IllegalArgumentException {
init();
return getData(clazz).enumTags;
}
/** Load defs from a properties file. */
private void load (InputStream is, String kind, ClassLoader cl) throws IOException {
Properties p = new Properties ();
try {
p.load (is);
} finally {
is.close ();
}
for (Map.Entry<String,String> entry : NbCollections.checkedMapByFilter(p, String.class, String.class, true).entrySet()) {
String name = entry.getKey();
if (kind.equals("type") && name.equals("description")) { // NOI18N
// Not a real data type; handled specially.
AntModule.err.log("Skipping pseudodef of <description>");
continue;
}
String clazzname = entry.getValue();
try {
Class<?> clazz = cl.loadClass (clazzname);
register(name, clazz, kind, false);
} catch (ClassNotFoundException cnfe) {
// This is normal, e.g. Ant's taskdefs include optional tasks we don't have.
AntModule.err.log ("IntrospectedInfo: skipping " + clazzname + ": " + cnfe);
} catch (NoClassDefFoundError ncdfe) {
// Normal for e.g. optional tasks which we cannot resolve against.
AntModule.err.log ("IntrospectedInfo: skipping " + clazzname + ": " + ncdfe);
} catch (LinkageError e) {
// Not normal; if it is there it ought to be resolvable etc.
throw (IOException) new IOException("Could not load class " + clazzname + ": " + e).initCause(e); // NOI18N
} catch (RuntimeException e) {
// SecurityException etc. Not normal.
throw (IOException) new IOException("Could not load class " + clazzname + ": " + e).initCause(e); // NOI18N
}
}
}
private void loadNetBeansSpecificDefinitions() {
loadNetBeansSpecificDefinitions0(AntBridge.getCustomDefsNoNamespace());
if (AntBridge.getInterface().isAnt16()) {
// Define both.
loadNetBeansSpecificDefinitions0(AntBridge.getCustomDefsWithNamespace());
}
}
private void loadNetBeansSpecificDefinitions0(Map<String,Map<String,Class>> defsByKind) {
for (Map.Entry<String,Map<String,Class>> kindE : defsByKind.entrySet()) {
for (Map.Entry<String,Class> defsE : kindE.getValue().entrySet()) {
register(defsE.getKey(), defsE.getValue(), kindE.getKey());
}
}
}
/** Register a new definition.
* May change the defined task/type for a given name, but
* will not redefine structure if classes are modified.
* Also any class definitions contained in the default map (if not this one)
* are just ignored; you should refer to the default map for info on them.
* Throws various errors if the class could not be resolved, e.g. NoClassDefFoundError.
* @param name name of the task or type as it appears in scripts
* @param clazz the implementing class
* @param kind the kind of definition to register (<code>task</code> or <code>type</code> currently)
* @since 2.4
*/
public synchronized void register(String name, Class clazz, String kind) {
register(name, clazz, kind, true);
}
private void register(String name, Class clazz, String kind, boolean fire) {
init();
synchronized (namedefs) {
Map<String,String> m = namedefs.get(kind);
if (m == null) {
m = new TreeMap<String,String>();
namedefs.put(kind, m);
}
m.put(name, clazz.getName());
}
boolean changed = analyze(clazz, null, false);
if (changed && fire) {
fireStateChanged();
}
}
/** Unregister a definition.
* Removes it from the definition mapping, though structural
* information about the implementing class (and classes referenced
* by that class) will not be removed.
* If the definition was not registered before, does nothing.
* @param name the definition name
* @param kind the kind of definition (<code>task</code> etc.)
* @since 2.4
*/
public synchronized void unregister(String name, String kind) {
init();
synchronized (namedefs) {
Map<String,String> m = namedefs.get(kind);
if (m != null) {
m.remove(name);
}
}
fireStateChanged();
}
/**
* Analyze a particular class and other classes recursively.
* Will never try to redefine anything in the default IntrospectedInfo.
* For custom IntrospectedInfo's, will never try to redefine anything
* if skipReanalysis is null. If not null, will not redefine anything
* in that set - so start recursion by passing an empty set, if you wish
* to redefine anything you come across recursively that is not in the
* default IntrospectedInfo, without causing loops.
* Attribute classes are examined just in case they are EnumeratedAttribute
* subclasses; they are not checked for subelements etc.
* Does not itself fire changes - you should do this if the return value is true.
* @param clazz the class to look at
* @param skipReanalysis null to do not redefs, or a set of already redef'd classes
* @param isAttrType false for an element class, true for an attribute class
* @return true if something changed
*/
private boolean analyze(Class clazz, Set<Class> skipReanalysis, boolean isAttrType) {
String n = clazz.getName();
/*
if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
AntModule.err.log("IntrospectedInfo.analyze: " + n + " skipping=" + skipReanalysis + " attrType=" + isAttrType);
}
*/
if (getDefaults().isKnown(n)) {
// Never try to redefine anything in the default IntrospectedInfo.
return false;
}
if ((skipReanalysis == null || !skipReanalysis.add(clazz)) && /* #23630 */isKnown(n)) {
// Either we are not redefining anything; or we are, but this class
// has already been in the list. Skip it. If we are continuing, make
// sure to add this class to the skip list so we do not loop.
return false;
}
//AntModule.err.log ("IntrospectedInfo.analyze: clazz=" + clazz.getName ());
//boolean dbg = (clazz == org.apache.tools.ant.taskdefs.Taskdef.class);
//if (! dbg && clazz.getName ().equals ("org.apache.tools.ant.taskdefs.Taskdef")) { // NOI18N
// AntModule.err.log ("Classloader mismatch: cl1=" + clazz.getClassLoader () + " cl2=" + org.apache.tools.ant.taskdefs.Taskdef.class.getClassLoader ());
//}
//if (dbg) AntModule.err.log ("Analyzing <taskdef> attrs...");
IntrospectedClass info = new IntrospectedClass ();
if (isAttrType) {
String[] enumTags = AntBridge.getInterface().getEnumeratedValues(clazz);
if (enumTags != null) {
info.enumTags = enumTags;
return !info.equals(clazzes.put(clazz.getName(), info));
} else {
// Do not store attr clazzes unless they are interesting: EnumAttr.
return clazzes.remove(clazz.getName()) != null;
}
// That's all we do - no subelements etc.
}
IntrospectionHelperProxy helper = AntBridge.getInterface().getIntrospectionHelper(clazz);
info.supportsText = helper.supportsCharacters ();
Enumeration<String> e = helper.getAttributes();
Set<Class> nueAttrTypeClazzes = new HashSet<Class>();
//if (dbg) AntModule.err.log ("Analyzing <taskdef> attrs...");
if (e.hasMoreElements ()) {
while (e.hasMoreElements ()) {
String name = e.nextElement();
//if (dbg) AntModule.err.log ("\tname=" + name);
try {
Class attrType = helper.getAttributeType(name);
String type = attrType.getName();
//if (dbg) AntModule.err.log ("\ttype=" + type);
if (hasSuperclass(clazz, "org.apache.tools.ant.Task") && // NOI18N
((name.equals ("location") && type.equals ("org.apache.tools.ant.Location")) || // NOI18N
(name.equals ("taskname") && type.equals ("java.lang.String")) || // NOI18N
(name.equals ("description") && type.equals ("java.lang.String")))) { // NOI18N
// IntrospectionHelper is supposed to exclude such things, but I guess not.
// Or it excludes location & taskType.
// description may be OK to actually show on nodes, but since it is common
// to all tasks it should not be stored as such. Ditto taskname.
continue;
}
// XXX also handle subclasses of DataType and its standard attrs
// incl. creating nicely-named node props for description, refid, etc.
if (info.attrs == null) {
info.attrs = new TreeMap<String,String>();
}
info.attrs.put (name, type);
nueAttrTypeClazzes.add(attrType);
} catch (RuntimeException re) { // i.e. BuildException; but avoid loading this class
AntModule.err.notify (ErrorManager.INFORMATIONAL, re);
}
}
} else {
info.attrs = null;
}
Set<Class> nueClazzes = new HashSet<Class>();
e = helper.getNestedElements ();
//if (dbg) AntModule.err.log ("Analyzing <taskdef> subels...");
if (e.hasMoreElements ()) {
while (e.hasMoreElements ()) {
String name = e.nextElement();
//if (dbg) AntModule.err.log ("\tname=" + name);
try {
Class subclazz = helper.getElementType (name);
//if (dbg) AntModule.err.log ("\ttype=" + subclazz.getName ());
if (info.subs == null) {
info.subs = new TreeMap<String,String>();
}
info.subs.put (name, subclazz.getName ());
nueClazzes.add (subclazz);
} catch (RuntimeException re) { // i.e. BuildException; but avoid loading this class
AntModule.err.notify (ErrorManager.INFORMATIONAL, re);
}
}
} else {
info.subs = null;
}
boolean changed = !info.equals(clazzes.put(clazz.getName(), info));
// And recursively analyze reachable classes for subelements...
// (usually these will already be known, and analyze will return at once)
for (Class nueClazz : nueClazzes) {
changed |= analyze(nueClazz, skipReanalysis, false);
}
for (Class nueClazz : nueAttrTypeClazzes) {
changed |= analyze(nueClazz, skipReanalysis, true);
}
return changed;
}
private static boolean hasSuperclass(Class subclass, String superclass) {
for (Class c = subclass; c != null; c = c.getSuperclass()) {
if (c.getName().equals(superclass)) {
return true;
}
}
return false;
}
/**
* Scan an existing (already-run) project to see if it has any new tasks/types.
* Any new definitions found will automatically be added to the known list.
* This will try to change existing definitions in the custom set, i.e.
* if a task is defined to be implemented with a different class, or if a
* class changes structure.
* Will not try to define anything contained in the defaults list.
* @param defs map from kinds to maps from names to classes
*/
public void scanProject(Map<String,Map<String,Class>> defs) {
init();
Set<Class> skipReanalysis = new HashSet<Class>();
boolean changed = false;
for (Map.Entry<String,Map<String,Class>> e : defs.entrySet()) {
changed |= scanMap(e.getValue(), e.getKey(), skipReanalysis);
}
if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
AntModule.err.log("IntrospectedInfo.scanProject: " + this);
}
if (changed) {
fireStateChanged();
}
}
private boolean scanMap(Map<String,Class> m, String kind, Set<Class> skipReanalysis) {
if (kind == null) throw new IllegalArgumentException();
boolean changed = false;
for (Map.Entry<String,Class> entry : m.entrySet()) {
String name = entry.getKey();
if (kind.equals("type") && name.equals("description")) { // NOI18N
// Not a real data type; handled specially.
AntModule.err.log("Skipping pseudodef of <description>");
continue;
}
Class<?> clazz = entry.getValue();
if (clazz.getName().equals("org.apache.tools.ant.taskdefs.MacroInstance")) { // NOI18N
continue;
}
Map<String,String> registry = namedefs.get(kind);
if (registry == null) {
registry = new TreeMap<String,String>();
namedefs.put(kind, registry);
}
synchronized (this) {
if (getDefaults().getDefs(kind).get(name) == null) {
changed |= !clazz.getName().equals(registry.put(name, clazz.getName()));
}
if (! getDefaults ().isKnown (clazz.getName ())) {
try {
changed |= analyze(clazz, skipReanalysis, false);
} catch (ThreadDeath td) {
throw td;
} catch (NoClassDefFoundError ncdfe) {
// Reasonably normal.
AntModule.err.log ("Skipping " + clazz.getName () + ": " + ncdfe);
} catch (LinkageError e) {
// Not so normal.
AntModule.err.annotate (e, ErrorManager.INFORMATIONAL, "Cannot scan class " + clazz.getName (), null, null, null); // NOI18N
AntModule.err.notify (ErrorManager.INFORMATIONAL, e);
}
}
}
}
return changed;
}
@Override
public String toString () {
return "IntrospectedInfo[namedefs=" + namedefs + ",clazzes=" + clazzes + "]"; // NOI18N
}
private static final class IntrospectedClass {
public boolean supportsText;
public Map<String,String> attrs; // null or name -> class
public Map<String,String> subs; // null or name -> class
public String[] enumTags; // null or list of tags
@Override
public String toString () {
return "IntrospectedClass[text=" + supportsText + ",attrs=" + attrs + ",subs=" + subs + ",enumTags=" + Arrays.toString(enumTags) + "]"; // NOI18N
}
@Override
public int hashCode() {
// XXX
return 0;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof IntrospectedClass)) {
return false;
}
IntrospectedClass other = (IntrospectedClass)o;
return supportsText == other.supportsText &&
Utilities.compareObjects(attrs, other.attrs) &&
Utilities.compareObjects(subs, other.subs) &&
Utilities.compareObjects(enumTags, other.enumTags);
}
}
// merging and including custom defs:
/** only used to permit use of WeakListener */
private ChangeListener holder;
/**
* Merge several IntrospectedInfo instances together.
* Responds live to updates.
*/
private static IntrospectedInfo merge(IntrospectedInfo[] proxied) {
final IntrospectedInfo ii = new IntrospectedInfo();
ChangeListener l = new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
IntrospectedInfo ii2 = (IntrospectedInfo)ev.getSource();
ii2.init();
ii.clazzes.putAll(ii2.clazzes);
for (Map.Entry<String,Map<String,String>> e : ii2.namedefs.entrySet()) {
String kind = e.getKey();
Map<String,String> entries = e.getValue();
if (ii.namedefs.containsKey(kind)) {
ii.namedefs.get(kind).putAll(entries);
} else {
ii.namedefs.put(kind, new TreeMap<String,String>(entries));
}
}
ii.fireStateChanged();
}
};
ii.holder = l;
for (IntrospectedInfo info : proxied) {
info.addChangeListener(WeakListeners.change(l, info));
l.stateChanged(new ChangeEvent(info));
}
return ii;
}
/** defaults + custom defs */
private static IntrospectedInfo merged;
/**
* Get all known introspected definitions.
* Includes all those in {@link #getDefaults} plus custom definitions
* encountered in actual build scripts (details unspecified).
* @return a set of all known definitions, e.g. of tasks and types
* @since 2.14
*/
public static synchronized IntrospectedInfo getKnownInfo() {
if (merged == null) {
merged = merge(new IntrospectedInfo[] {
getDefaults(),
AntSettings.getCustomDefs(),
});
}
return merged;
}
static {
AntSettings.IntrospectedInfoSerializer.instance = new AntSettings.IntrospectedInfoSerializer() {
/*
Format quick key:
Map<String,Map<String,String>> namedefs: task.echo=org.apache.tools.ant.taskdefs.Echo
Map<String,IntrospectedClass> clazzes: class.org.apache.tools.ant.taskdefs.Echo.<...>
boolean supportsText: .supportsText=true
null | Map<String,String> attrs: .attrs.message=java.lang.String
null | Map<String,String> subs: .subs.file=java.io.File
null | String[] enumTags: .enumTags=whenempty,always,never
*/
Pattern p = Pattern.compile("(.+)\\.(supportsText|attrs\\.(.+)|subs\\.(.+)|enumTags)");
public IntrospectedInfo load(Preferences node) {
IntrospectedInfo ii = new IntrospectedInfo();
try {
for (String k : node.keys()) {
String v = node.get(k, null);
assert v != null : k;
String[] ss = k.split("\\.", 2);
if (ss.length != 2) {
LOG.log(Level.WARNING, "malformed key: {0}", k);
continue;
}
if (ss[0].equals("class")) {
Matcher m = p.matcher(ss[1]);
boolean match = m.matches();
if (!match) {
LOG.log(Level.WARNING, "malformed key: {0}", k);
continue;
}
String c = m.group(1);
IntrospectedClass ic = assureDefined(ii, c);
String tail = m.group(2);
if (tail.equals("supportsText")) {
assert v.equals("true") : k;
ic.supportsText = true;
} else if (tail.equals("enumTags")) {
ic.enumTags = v.split(",");
} else if (m.group(3) != null) {
if (ic.attrs == null) {
ic.attrs = new TreeMap<String,String>();
}
ic.attrs.put(m.group(3), v);
//assureDefined(ii, v);
} else {
assert m.group(4) != null : k;
if (ic.subs == null) {
ic.subs = new TreeMap<String,String>();
}
ic.subs.put(m.group(4), v);
//assureDefined(ii, v);
}
} else {
Map<String,String> m = ii.namedefs.get(ss[0]);
if (m == null) {
m = new TreeMap<String,String>();
ii.namedefs.put(ss[0], m);
}
m.put(ss[1], v);
//assureDefined(ii, v);
}
}
} catch (BackingStoreException x) {
LOG.log(Level.WARNING, null, x);
}
for (String kind : new String[] {"task", "type"}) {
if (!ii.namedefs.containsKey(kind)) {
ii.namedefs.put(kind, new TreeMap<String,String>());
}
}
return ii;
}
private IntrospectedClass assureDefined(IntrospectedInfo ii, String clazz) {
IntrospectedClass ic = ii.clazzes.get(clazz);
if (ic == null) {
ic = new IntrospectedClass();
ii.clazzes.put(clazz, ic);
}
return ic;
}
public void store(Preferences node, IntrospectedInfo info) {
try {
node.clear();
} catch (BackingStoreException x) {
LOG.log(Level.WARNING, null, x);
return;
}
for (Map.Entry<String,Map<String,String>> kindEntries : info.namedefs.entrySet()) {
for (Map.Entry<String,String> namedef : kindEntries.getValue().entrySet()) {
node.put(kindEntries.getKey() + "." + namedef.getKey(), namedef.getValue());
}
}
for (Map.Entry<String,IntrospectedClass> clazzPair : info.clazzes.entrySet()) {
String c = "class." + clazzPair.getKey();
IntrospectedClass ic = clazzPair.getValue();
if (ic.supportsText) {
node.putBoolean(c + ".supportsText", true);
}
if (ic.attrs != null) {
for (Map.Entry<String,String> attr : ic.attrs.entrySet()) {
node.put(c + ".attrs." + attr.getKey(), attr.getValue());
}
}
if (ic.subs != null) {
for (Map.Entry<String,String> sub : ic.subs.entrySet()) {
node.put(c + ".subs." + sub.getKey(), sub.getValue());
}
}
if (ic.enumTags != null) {
StringBuilder b = new StringBuilder();
for (String s : ic.enumTags) {
if (b.length() > 0) {
b.append(',');
}
b.append(s);
}
node.put(c + ".enumTags", b.toString());
}
}
// Exact equivalence is unlikely to happen; there may be unanalyzed Java classes, etc.
//assert equiv(info, load(node)) : info + " vs. " + load(node);
}
private boolean equiv(IntrospectedInfo ii1, IntrospectedInfo ii2) {
return ii1.clazzes.equals(ii2.clazzes) && ii1.namedefs.equals(ii2.namedefs);
}
};
}
}