/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.ruby.spi.project.support.rake; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.netbeans.api.project.ProjectManager; import org.openide.ErrorManager; import org.openide.filesystems.FileAttributeEvent; import org.openide.filesystems.FileChangeListener; import org.openide.filesystems.FileEvent; import org.openide.filesystems.FileLock; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileRenameEvent; import org.openide.filesystems.FileUtil; import org.openide.util.EditableProperties; import org.openide.util.Mutex; import org.openide.util.MutexException; import org.openide.util.NbCollections; import org.openide.util.RequestProcessor; import org.openide.util.TopologicalSortException; import org.openide.util.Union2; import org.openide.util.Utilities; import org.openide.util.WeakListeners; /** * Support for working with Ant properties and property files. * @author Jesse Glick */ public class PropertyUtils { private PropertyUtils() {} /** * Location in user directory of per-user global properties. * May be null if <code>netbeans.user</code> is not set. */ static File userBuildProperties() { String nbuser = System.getProperty("netbeans.user"); // NOI18N if (nbuser != null) { return FileUtil.normalizeFile(new File(nbuser, "build.properties")); // NOI18N } else { return null; } } private static Map<File,Reference<PropertyProvider>> globalPropertyProviders = new HashMap<File,Reference<PropertyProvider>>(); /** * Load global properties defined by the IDE in the user directory. * Currently loads ${netbeans.user}/build.properties if it exists. * <p> * Acquires read access. * <p> * To listen to changes use {@link #globalPropertyProvider}. * @return user properties (empty if missing or malformed) */ public static EditableProperties getGlobalProperties() { return ProjectManager.mutex().readAccess(new Mutex.Action<EditableProperties>() { public EditableProperties run() { File ubp = userBuildProperties(); if (ubp != null && ubp.isFile() && ubp.canRead()) { try { InputStream is = new FileInputStream(ubp); try { EditableProperties properties = new EditableProperties(true); properties.load(is); return properties; } finally { is.close(); } } catch (IOException e) { Logger.getLogger(PropertyUtils.class.getName()).log(Level.INFO, null, e); } } // Missing or erroneous. return new EditableProperties(true); } }); } /** * Edit global properties defined by the IDE in the user directory. * <p> * Acquires write access. * @param properties user properties to set * @throws IOException if they could not be stored * @see #getGlobalProperties */ public static void putGlobalProperties(final EditableProperties properties) throws IOException { try { ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() { public Void run() throws IOException { File ubp = userBuildProperties(); if (ubp != null) { FileObject bp = FileUtil.toFileObject(ubp); if (bp == null) { if (!ubp.exists()) { FileObject folder = FileUtil.createFolder(ubp.getParentFile()); folder.createData(ubp.getName()); assert ubp.isFile() : "Did not actually make " + ubp; } bp = FileUtil.toFileObject(ubp); if (bp == null) { // XXX ugly (and will not correctly notify changes) but better than nothing: ErrorManager.getDefault().log(ErrorManager.WARNING, "Warning - cannot properly write to " + ubp + "; might be because your user directory is on a Windows UNC path (issue #46813)? If so, try using mapped drive letters."); OutputStream os = new FileOutputStream(ubp); try { properties.store(os); } finally { os.close(); } return null; } } FileLock lock = bp.lock(); try { OutputStream os = bp.getOutputStream(lock); try { properties.store(os); } finally { os.close(); } } finally { lock.releaseLock(); } } else { throw new IOException("Do not know where to store build.properties; must set netbeans.user!"); // NOI18N } return null; } }); } catch (MutexException e) { throw (IOException)e.getException(); } } /** * Create a property evaluator based on {@link #getGlobalProperties} * and {@link #putGlobalProperties}. * It will supply global properties and fire changes when this file * is changed. * @return a property producer */ public static synchronized PropertyProvider globalPropertyProvider() { File ubp = userBuildProperties(); if (ubp != null) { Reference<PropertyProvider> globalPropertyProvider = globalPropertyProviders.get(ubp); if (globalPropertyProvider != null) { PropertyProvider pp = globalPropertyProvider.get(); if (pp != null) { return pp; } } PropertyProvider gpp = propertiesFilePropertyProvider(ubp); globalPropertyProviders.put(ubp, new SoftReference<PropertyProvider>(gpp)); return gpp; } else { return fixedPropertyProvider(Collections.<String,String>emptyMap()); } } /** * Create a property provider based on a properties file. * The file need not exist at the moment; if it is created or deleted an appropriate * change will be fired. If its contents are changed on disk a change will also be fired. * @param propertiesFile a path to a (possibly nonexistent) *.properties file * @return a supplier of properties from such a file * @see Properties#load */ public static PropertyProvider propertiesFilePropertyProvider(File propertiesFile) { assert propertiesFile != null; return new FilePropertyProvider(propertiesFile); } /** * Provider based on a named properties file. */ private static final class FilePropertyProvider implements PropertyProvider, FileChangeListener { private static final RequestProcessor RP = new RequestProcessor("PropertyUtils.FilePropertyProvider.RP"); // NOI18N private final File properties; private final List<ChangeListener> listeners = new ArrayList<ChangeListener>(); private Map<String,String> cached = null; private long cachedTime = 0L; public FilePropertyProvider(File properties) { this.properties = properties; FileUtil.addFileChangeListener(this, properties); } public Map<String,String> getProperties() { long currTime = properties.lastModified(); if (cached == null || cachedTime != currTime) { cachedTime = currTime; cached = loadProperties(); } return cached; } private Map<String,String> loadProperties() { // XXX does this need to run in PM.mutex.readAccess? if (properties.isFile() && properties.canRead()) { try { InputStream is = new FileInputStream(properties); try { Properties props = new Properties(); props.load(is); return NbCollections.checkedMapByFilter(props, String.class, String.class, true); } finally { is.close(); } } catch (IOException e) { Logger.getLogger(PropertyUtils.class.getName()).log(Level.INFO, null, e); } } // Missing or erroneous. return Collections.emptyMap(); } private void fireChange() { cachedTime = -1L; // force reload final ChangeListener[] ls; synchronized (this) { if (listeners.isEmpty()) { return; } ls = listeners.toArray(new ChangeListener[listeners.size()]); } final ChangeEvent ev = new ChangeEvent(this); final Mutex.Action<Void> action = new Mutex.Action<Void>() { public Void run() { for (ChangeListener l : ls) { l.stateChanged(ev); } return null; } }; if (ProjectManager.mutex().isWriteAccess()) { // Run it right now. postReadRequest would be too late. ProjectManager.mutex().readAccess(action); } else if (ProjectManager.mutex().isReadAccess()) { // Run immediately also. No need to switch to read access. action.run(); } else { // Not safe to acquire a new lock, so run later in read access. RP.post(new Runnable() { public void run() { ProjectManager.mutex().readAccess(action); } }); } } public synchronized void addChangeListener(ChangeListener l) { listeners.add(l); } public synchronized void removeChangeListener(ChangeListener l) { listeners.remove(l); } public void fileFolderCreated(FileEvent fe) { fireChange(); } public void fileDataCreated(FileEvent fe) { fireChange(); } public void fileChanged(FileEvent fe) { fireChange(); } public void fileDeleted(FileEvent fe) { fireChange(); } public void fileRenamed(FileRenameEvent fe) { fireChange(); } public void fileAttributeChanged(FileAttributeEvent fe) { fireChange(); } public String toString() { return "FilePropertyProvider[" + properties + ":" + getProperties() + "]"; // NOI18N } } /** * Evaluate all properties in a list of property mappings. * <p> * If there are any cyclic definitions within a single mapping, * the evaluation will fail and return null. * @param defs an ordered list of property mappings, e.g. {@link EditableProperties} instances * @param predefs an unevaluated set of initial definitions * @return values for all defined properties, or null if a circularity error was detected */ private static Map<String,String> evaluateAll(Map<String,String> predefs, List<Map<String,String>> defs) { Map<String,String> m = new HashMap<String,String>(predefs); for (Map<String,String> curr : defs) { // Set of properties which we are deferring because they subst sibling properties: Map<String,Set<String>> dependOnSiblings = new HashMap<String,Set<String>>(); for (Map.Entry<String,String> entry : curr.entrySet()) { String prop = entry.getKey(); if (!m.containsKey(prop)) { String rawval = entry.getValue(); //System.err.println("subst " + prop + "=" + rawval + " with " + m); Union2<String,Set<String>> o = substitute(rawval, m, curr.keySet()); if (o.hasFirst()) { m.put(prop, o.first()); } else { dependOnSiblings.put(prop, o.second()); } } } Set<String> toSort = new HashSet<String>(dependOnSiblings.keySet()); for (Set<String> s : dependOnSiblings.values()) { toSort.addAll(s); } List<String> sorted; try { sorted = Utilities.topologicalSort(toSort, dependOnSiblings); } catch (TopologicalSortException e) { //System.err.println("Cyclic property refs: " + Arrays.asList(e.unsortableSets())); return null; } Collections.reverse(sorted); for (String prop : sorted) { if (!m.containsKey(prop)) { String rawval = curr.get(prop); m.put(prop, substitute(rawval, m, /*Collections.EMPTY_SET*/curr.keySet()).first()); } } } return m; } /** * Try to substitute property references etc. in an Ant property value string. * @param rawval the raw value to be substituted * @param predefs a set of properties already defined * @param siblingProperties a set of property names that are yet to be defined * @return either a String, in case everything can be evaluated now; * or a Set<String> of elements from siblingProperties in case those properties * need to be defined in order to evaluate this one */ private static Union2<String,Set<String>> substitute(String rawval, Map<String,String> predefs, Set<String> siblingProperties) { assert rawval != null : "null rawval passed in"; if (rawval.indexOf('$') == -1) { // Shortcut: //System.err.println("shortcut"); return Union2.createFirst(rawval); } // May need to subst something. int idx = 0; // Result in progress, if it is to be a String: StringBuffer val = new StringBuffer(); // Or, result in progress, if it is to be a Set<String>: Set<String> needed = new HashSet<String>(); while (true) { int shell = rawval.indexOf('$', idx); if (shell == -1 || shell == rawval.length() - 1) { // No more $, or only as last char -> copy all. //System.err.println("no more $"); if (needed.isEmpty()) { val.append(rawval.substring(idx)); return Union2.createFirst(val.toString()); } else { return Union2.createSecond(needed); } } char c = rawval.charAt(shell + 1); if (c == '$') { // $$ -> $ //System.err.println("$$"); if (needed.isEmpty()) { val.append('$'); } idx += 2; } else if (c == '{') { // Possibly a property ref. int end = rawval.indexOf('}', shell + 2); if (end != -1) { // Definitely a property ref. String otherprop = rawval.substring(shell + 2, end); //System.err.println("prop ref to " + otherprop); if (predefs.containsKey(otherprop)) { // Well-defined. if (needed.isEmpty()) { val.append(rawval.substring(idx, shell)); val.append(predefs.get(otherprop)); } idx = end + 1; } else if (siblingProperties.contains(otherprop)) { needed.add(otherprop); // don't bother updating val, it will not be used anyway idx = end + 1; } else { // No def, leave as is. if (needed.isEmpty()) { val.append(rawval.substring(idx, end + 1)); } idx = end + 1; } } else { // Unclosed ${ sequence, leave as is. if (needed.isEmpty()) { val.append(rawval.substring(idx)); return Union2.createFirst(val.toString()); } else { return Union2.createSecond(needed); } } } else { // $ followed by some other char, leave as is. // XXX is this actually right? if (needed.isEmpty()) { val.append(rawval.substring(idx, idx + 2)); } idx += 2; } } } private static final Pattern RELATIVE_SLASH_SEPARATED_PATH = Pattern.compile("[^:/\\\\.][^:/\\\\]*(/[^:/\\\\.][^:/\\\\]*)*"); // NOI18N /** * Find an absolute file path from a possibly relative path. * @param basedir base file for relative filename resolving; must be an absolute path * @param filename a pathname which may be relative or absolute and may * use / or \ as the path separator * @return an absolute file corresponding to it * @throws IllegalArgumentException if basedir is not absolute */ public static File resolveFile(File basedir, String filename) throws IllegalArgumentException { if (basedir == null) { throw new NullPointerException("null basedir passed to resolveFile"); // NOI18N } if (filename == null) { throw new NullPointerException("null filename passed to resolveFile"); // NOI18N } if (!basedir.isAbsolute()) { throw new IllegalArgumentException("nonabsolute basedir passed to resolveFile: " + basedir); // NOI18N } File f; if (RELATIVE_SLASH_SEPARATED_PATH.matcher(filename).matches()) { // Shortcut - simple relative path. Potentially faster. f = new File(basedir, filename.replace('/', File.separatorChar)); } else { // All other cases. String machinePath = filename.replace('/', File.separatorChar).replace('\\', File.separatorChar); f = new File(machinePath); if (!f.isAbsolute()) { f = new File(basedir, machinePath); } assert f.isAbsolute(); } return FileUtil.normalizeFile(f); } /** * Produce a machine-independent relativized version of a filename from a basedir. * Unlike {@link URI#relativize} this will produce "../" sequences as needed. * @param basedir a directory to resolve relative to (need not exist on disk) * @param file a file or directory to find a relative path for * @return a relativized path (slash-separated), or null if it is not possible (e.g. different DOS drives); * just <samp>.</samp> in case the paths are the same * @throws IllegalArgumentException if the basedir is known to be a file and not a directory */ public static String relativizeFile(File basedir, File file) { if (basedir.isFile()) { throw new IllegalArgumentException("Cannot relative w.r.t. a data file " + basedir); // NOI18N } if (basedir.equals(file)) { return "."; // NOI18N } StringBuffer b = new StringBuffer(); File base = basedir; String filepath = file.getAbsolutePath(); while (!filepath.startsWith(slashify(base.getAbsolutePath()))) { base = base.getParentFile(); if (base == null) { return null; } if (base.equals(file)) { // #61687: file is a parent of basedir b.append(".."); // NOI18N return b.toString(); } b.append("../"); // NOI18N } URI u = base.toURI().relativize(file.toURI()); assert !u.isAbsolute() : u + " from " + basedir + " and " + file + " with common root " + base; b.append(u.getPath()); if (b.charAt(b.length() - 1) == '/') { // file is an existing directory and file.toURI ends in / // we do not want the trailing slash b.setLength(b.length() - 1); } return b.toString(); } private static String slashify(String path) { if (path.endsWith(File.separator)) { return path; } else { return path + File.separatorChar; } } /*public? */ static FileObject resolveFileObject(FileObject basedir, String filename) { if (RELATIVE_SLASH_SEPARATED_PATH.matcher(filename).matches()) { // Shortcut. Potentially much faster. return basedir.getFileObject(filename); } else { // Might be an absolute path, or \-separated, or . or .. components, etc.; use the safer method. return FileUtil.toFileObject(resolveFile(FileUtil.toFile(basedir), filename)); } } /*public? */ static String resolvePath(File basedir, String path) { StringBuffer b = new StringBuffer(); String[] toks = tokenizePath(path); for (int i = 0; i < toks.length; i++) { if (i > 0) { b.append(File.pathSeparatorChar); } b.append(resolveFile(basedir, toks[i]).getAbsolutePath()); } return b.toString(); } /** * Split an Ant-style path specification into components. * Tokenizes on <code>:</code> and <code>;</code>, paying * attention to DOS-style components such as <samp>C:\FOO</samp>. * Also removes any empty components. * @param path an Ant-style path (elements arbitrary) using DOS or Unix separators * @return a tokenization of that path into components */ public static String[] tokenizePath(String path) { List<String> l = new ArrayList<String>(); StringTokenizer tok = new StringTokenizer(path, ":;", true); // NOI18N char dosHack = '\0'; char lastDelim = '\0'; int delimCount = 0; while (tok.hasMoreTokens()) { String s = tok.nextToken(); if (s.length() == 0) { // Strip empty components. continue; } if (s.length() == 1) { char c = s.charAt(0); if (c == ':' || c == ';') { // Just a delimiter. lastDelim = c; delimCount++; continue; } } if (dosHack != '\0') { // #50679 - "C:/something" is also accepted as DOS path if (lastDelim == ':' && delimCount == 1 && (s.charAt(0) == '\\' || s.charAt(0) == '/')) { // We had a single letter followed by ':' now followed by \something or /something s = "" + dosHack + ':' + s; // and use the new token with the drive prefix... } else { // Something else, leave alone. l.add(Character.toString(dosHack)); // and continue with this token too... } dosHack = '\0'; } // Reset count of # of delimiters in a row. delimCount = 0; if (s.length() == 1) { char c = s.charAt(0); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { // Probably a DOS drive letter. Leave it with the next component. dosHack = c; continue; } } l.add(s); } if (dosHack != '\0') { //the dosHack was the last letter in the input string (not followed by the ':') //so obviously not a drive letter. //Fix for issue #57304 l.add(Character.toString(dosHack)); } return l.toArray(new String[l.size()]); } private static final Pattern VALID_PROPERTY_NAME = Pattern.compile("[-._a-zA-Z0-9]"); // NOI18N /** * Checks whether the name is usable as Ant property name. * @param name name to check for usability as Ant property * @return true if name is usable otherwise false */ public static boolean isUsablePropertyName(String name) { return VALID_PROPERTY_NAME.matcher(name).matches(); } /** * Returns name usable as Ant property which is based on the given * name. All forbidden characters are either removed or replaced with * suitable ones. * @param name name to use as base for Ant property name * @return name usable as Ant property name */ public static String getUsablePropertyName(String name) { if (isUsablePropertyName(name)) { return name; } StringBuffer sb = new StringBuffer(name); for (int i=0; i<sb.length(); i++) { if (!isUsablePropertyName(sb.substring(i,i+1))) { sb.replace(i,i+1,"_"); } } return sb.toString(); } /** * Create a trivial property producer using only a fixed list of property definitions. * Its values are constant, and it never fires changes. * @param defs a map from property names to values (it is illegal to modify this map * after passing it to this method) * @return a matching property producer */ public static PropertyProvider fixedPropertyProvider(Map<String,String> defs) { return new FixedPropertyProvider(defs); } private static final class FixedPropertyProvider implements PropertyProvider { private final Map<String,String> defs; public FixedPropertyProvider(Map<String,String> defs) { this.defs = defs; } public Map<String,String> getProperties() { return defs; } public void addChangeListener(ChangeListener l) {} public void removeChangeListener(ChangeListener l) {} } /** * Create a property evaluator based on a series of definitions. * <p> * Each batch of definitions can refer to properties within itself * (so long as there is no cycle) or any previous batch. * However the special first provider cannot refer to properties within itself. * </p> * <p> * This implementation acquires {@link ProjectManager#mutex} for all operations, in read mode, * and fires changes synchronously. It also expects changes to be fired from property * providers in read (or write) access. * </p> * @param preprovider an initial context (may be null) * @param providers a sequential list of property groups * @return an evaluator */ public static PropertyEvaluator sequentialPropertyEvaluator(PropertyProvider preprovider, PropertyProvider... providers) { return new SequentialPropertyEvaluator(preprovider, providers); } /** * Creates a property provider similar to {@link #globalPropertyProvider} * but which can use a different global properties file. * If a specific file is pointed to, that is loaded; otherwise behaves like {@link #globalPropertyProvider}. * Permits behavior similar to command-line Ant where not erroneous, but using the IDE's * default global properties for projects which do not yet have this property registered. * @param findUserPropertiesFile an evaluator in which to look up <code>propertyName</code> * @param propertyName a property pointing to the global properties file (typically <code>"user.properties.file"</code>) * @param basedir a base directory to use when resolving the path to the global properties file, if relative * @return a provider of global properties * @since org.netbeans.modules.ruby.modules.project.rake/1 1.14 */ public static PropertyProvider userPropertiesProvider(PropertyEvaluator findUserPropertiesFile, String propertyName, File basedir) { return new UserPropertiesProvider(findUserPropertiesFile, propertyName, basedir); } private static final class UserPropertiesProvider extends FilterPropertyProvider implements PropertyChangeListener { private final PropertyEvaluator findUserPropertiesFile; private final String propertyName; private final File basedir; public UserPropertiesProvider(PropertyEvaluator findUserPropertiesFile, String propertyName, File basedir) { super(computeDelegate(findUserPropertiesFile, propertyName, basedir)); this.findUserPropertiesFile = findUserPropertiesFile; this.propertyName = propertyName; this.basedir = basedir; findUserPropertiesFile.addPropertyChangeListener(this); } public void propertyChange(PropertyChangeEvent ev) { if (propertyName.equals(ev.getPropertyName())) { setDelegate(computeDelegate(findUserPropertiesFile, propertyName, basedir)); } } private static PropertyProvider computeDelegate(PropertyEvaluator findUserPropertiesFile, String propertyName, File basedir) { String userPropertiesFile = findUserPropertiesFile.getProperty(propertyName); if (userPropertiesFile != null) { // Have some defined global properties file, so read it and listen to changes in it. File f = PropertyUtils.resolveFile(basedir, userPropertiesFile); if (f.equals(PropertyUtils.userBuildProperties())) { // Just to share the cache. return PropertyUtils.globalPropertyProvider(); } else { return PropertyUtils.propertiesFilePropertyProvider(f); } } else { // Use the in-IDE default. return PropertyUtils.globalPropertyProvider(); } } } private static final class SequentialPropertyEvaluator implements PropertyEvaluator, ChangeListener { private final PropertyProvider preprovider; private final PropertyProvider[] providers; private Map<String,String> defs; private final List<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>(); public SequentialPropertyEvaluator(final PropertyProvider preprovider, final PropertyProvider[] providers) { this.preprovider = preprovider; this.providers = providers; // XXX defer until someone asks for them defs = ProjectManager.mutex().readAccess(new Mutex.Action<Map<String,String>>() { public Map<String,String> run() { return compose(preprovider, providers); } }); // XXX defer until someone is listening? if (preprovider != null) { preprovider.addChangeListener(WeakListeners.change(this, preprovider)); } for (PropertyProvider pp : providers) { pp.addChangeListener(WeakListeners.change(this, pp)); } } public String getProperty(final String prop) { return ProjectManager.mutex().readAccess(new Mutex.Action<String>() { public String run() { if (defs == null) { return null; } return defs.get(prop); } }); } public String evaluate(final String text) { if (text == null) { throw new NullPointerException("Attempted to pass null to PropertyEvaluator.evaluate"); // NOI18N } return ProjectManager.mutex().readAccess(new Mutex.Action<String>() { public String run() { if (defs == null) { return null; } Union2<String,Set<String>> result = substitute(text, defs, Collections.<String>emptySet()); assert result.hasFirst() : "Unexpected result " + result + " from " + text + " on " + defs; return result.first(); } }); } public Map<String,String> getProperties() { return ProjectManager.mutex().readAccess(new Mutex.Action<Map<String,String>>() { public Map<String,String> run() { return defs; } }); } public void addPropertyChangeListener(PropertyChangeListener listener) { synchronized (listeners) { listeners.add(listener); } } public void removePropertyChangeListener(PropertyChangeListener listener) { synchronized (listeners) { listeners.remove(listener); } } public void stateChanged(ChangeEvent e) { assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); Map<String,String> newdefs = compose(preprovider, providers); // compose() may return null upon circularity errors Map<String,String> _defs = defs != null ? defs : Collections.<String,String>emptyMap(); Map<String,String> _newdefs = newdefs != null ? newdefs : Collections.<String,String>emptyMap(); if (!_defs.equals(_newdefs)) { Set<String> props = new HashSet<String>(_defs.keySet()); props.addAll(_newdefs.keySet()); List<PropertyChangeEvent> events = new LinkedList<PropertyChangeEvent>(); for (String prop : props) { assert prop != null; String oldval = _defs.get(prop); String newval = _newdefs.get(prop); if (newval != null) { if (newval.equals(oldval)) { continue; } } else { assert oldval != null : "should not have had " + prop; } events.add(new PropertyChangeEvent(this, prop, oldval, newval)); } assert !events.isEmpty(); defs = newdefs; PropertyChangeListener[] _listeners; synchronized (listeners) { _listeners = listeners.toArray(new PropertyChangeListener[listeners.size()]); } for (PropertyChangeListener l : _listeners) { for (PropertyChangeEvent ev : events) { l.propertyChange(ev); } } } } private static Map<String,String> compose(PropertyProvider preprovider, PropertyProvider[] providers) { assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess(); Map<String,String> predefs; if (preprovider != null) { predefs = preprovider.getProperties(); } else { predefs = Collections.emptyMap(); } List<Map<String,String>> defs = new ArrayList<Map<String,String>>(providers.length); for (PropertyProvider pp : providers) { defs.add(pp.getProperties()); } return evaluateAll(predefs, defs); } } }