2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.spi.project.support.ant;
44
import java.beans.PropertyChangeEvent;
45
import java.beans.PropertyChangeListener;
47
import java.io.FileInputStream;
48
import java.io.FileOutputStream;
49
import java.io.IOException;
50
import java.io.InputStream;
51
import java.io.OutputStream;
52
import java.lang.ref.Reference;
53
import java.lang.ref.SoftReference;
55
import java.util.ArrayList;
56
import java.util.Collections;
57
import java.util.HashMap;
58
import java.util.HashSet;
59
import java.util.LinkedList;
60
import java.util.List;
62
import java.util.Properties;
64
import java.util.StringTokenizer;
65
import java.util.logging.Level;
66
import java.util.logging.Logger;
67
import java.util.regex.Pattern;
68
import javax.swing.event.ChangeEvent;
69
import javax.swing.event.ChangeListener;
70
import org.netbeans.api.project.ProjectManager;
71
import org.netbeans.modules.project.ant.FileChangeSupport;
72
import org.netbeans.modules.project.ant.FileChangeSupportEvent;
73
import org.netbeans.modules.project.ant.FileChangeSupportListener;
74
import org.openide.ErrorManager;
75
import org.openide.filesystems.FileLock;
76
import org.openide.filesystems.FileObject;
77
import org.openide.filesystems.FileUtil;
78
import org.openide.util.ChangeSupport;
79
import org.openide.util.Mutex;
80
import org.openide.util.MutexException;
81
import org.openide.util.NbCollections;
82
import org.openide.util.RequestProcessor;
83
import org.openide.util.TopologicalSortException;
84
import org.openide.util.Union2;
85
import org.openide.util.Utilities;
86
import org.openide.util.WeakListeners;
89
* Support for working with Ant properties and property files.
92
public class PropertyUtils {
94
private PropertyUtils() {}
97
* Location in user directory of per-user global properties.
98
* May be null if <code>netbeans.user</code> is not set.
100
static File userBuildProperties() {
101
String nbuser = System.getProperty("netbeans.user"); // NOI18N
102
if (nbuser != null) {
103
return FileUtil.normalizeFile(new File(nbuser, "build.properties")); // NOI18N
109
private static Map<File,Reference<PropertyProvider>> globalPropertyProviders = new HashMap<File,Reference<PropertyProvider>>();
112
* Load global properties defined by the IDE in the user directory.
113
* Currently loads ${netbeans.user}/build.properties if it exists.
115
* Acquires read access.
117
* To listen to changes use {@link #globalPropertyProvider}.
118
* @return user properties (empty if missing or malformed)
120
public static EditableProperties getGlobalProperties() {
121
return ProjectManager.mutex().readAccess(new Mutex.Action<EditableProperties>() {
122
public EditableProperties run() {
123
File ubp = userBuildProperties();
124
if (ubp != null && ubp.isFile() && ubp.canRead()) {
126
InputStream is = new FileInputStream(ubp);
128
EditableProperties properties = new EditableProperties(true);
134
} catch (IOException e) {
135
Logger.getLogger(PropertyUtils.class.getName()).log(Level.INFO, null, e);
138
// Missing or erroneous.
139
return new EditableProperties(true);
145
* Edit global properties defined by the IDE in the user directory.
147
* Acquires write access.
148
* @param properties user properties to set
149
* @throws IOException if they could not be stored
150
* @see #getGlobalProperties
152
public static void putGlobalProperties(final EditableProperties properties) throws IOException {
154
ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
155
public Void run() throws IOException {
156
File ubp = userBuildProperties();
158
FileObject bp = FileUtil.toFileObject(ubp);
161
ubp.getParentFile().mkdirs();
162
new FileOutputStream(ubp).close();
163
assert ubp.isFile() : "Did not actually make " + ubp;
165
bp = FileUtil.toFileObject(ubp);
167
// XXX ugly (and will not correctly notify changes) but better than nothing:
168
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.");
169
OutputStream os = new FileOutputStream(ubp);
171
properties.store(os);
178
FileLock lock = bp.lock();
180
OutputStream os = bp.getOutputStream(lock);
182
properties.store(os);
190
throw new IOException("Do not know where to store build.properties; must set netbeans.user!"); // NOI18N
195
} catch (MutexException e) {
196
throw (IOException)e.getException();
201
* Create a property evaluator based on {@link #getGlobalProperties}
202
* and {@link #putGlobalProperties}.
203
* It will supply global properties and fire changes when this file
205
* @return a property producer
207
public static synchronized PropertyProvider globalPropertyProvider() {
208
File ubp = userBuildProperties();
210
Reference<PropertyProvider> globalPropertyProvider = globalPropertyProviders.get(ubp);
211
if (globalPropertyProvider != null) {
212
PropertyProvider pp = globalPropertyProvider.get();
217
PropertyProvider gpp = propertiesFilePropertyProvider(ubp);
218
globalPropertyProviders.put(ubp, new SoftReference<PropertyProvider>(gpp));
221
return fixedPropertyProvider(Collections.<String,String>emptyMap());
226
* Create a property provider based on a properties file.
227
* The file need not exist at the moment; if it is created or deleted an appropriate
228
* change will be fired. If its contents are changed on disk a change will also be fired.
229
* @param propertiesFile a path to a (possibly nonexistent) *.properties file
230
* @return a supplier of properties from such a file
231
* @see Properties#load
233
public static PropertyProvider propertiesFilePropertyProvider(File propertiesFile) {
234
assert propertiesFile != null;
235
return new FilePropertyProvider(propertiesFile);
239
* Provider based on a named properties file.
241
private static final class FilePropertyProvider implements PropertyProvider, FileChangeSupportListener {
243
private static final RequestProcessor RP = new RequestProcessor("PropertyUtils.FilePropertyProvider.RP"); // NOI18N
245
private final File properties;
246
private final ChangeSupport cs = new ChangeSupport(this);
247
private Map<String,String> cached = null;
248
private long cachedTime = 0L;
250
public FilePropertyProvider(File properties) {
251
this.properties = properties;
252
FileChangeSupport.DEFAULT.addListener(this, properties);
255
public Map<String,String> getProperties() {
256
long currTime = properties.lastModified();
257
if (cached == null || cachedTime != currTime) {
258
cachedTime = currTime;
259
cached = loadProperties();
264
private Map<String,String> loadProperties() {
265
// XXX does this need to run in PM.mutex.readAccess?
266
if (properties.isFile() && properties.canRead()) {
268
InputStream is = new FileInputStream(properties);
270
Properties props = new Properties();
272
return NbCollections.checkedMapByFilter(props, String.class, String.class, true);
276
} catch (IOException e) {
277
Logger.getLogger(PropertyUtils.class.getName()).log(Level.INFO, null, e);
280
// Missing or erroneous.
281
return Collections.emptyMap();
284
private void fireChange() {
285
cachedTime = -1L; // force reload
286
if (!cs.hasListeners()) {
289
final Mutex.Action<Void> action = new Mutex.Action<Void>() {
295
if (ProjectManager.mutex().isWriteAccess()) {
296
// Run it right now. postReadRequest would be too late.
297
ProjectManager.mutex().readAccess(action);
298
} else if (ProjectManager.mutex().isReadAccess()) {
299
// Run immediately also. No need to switch to read access.
302
// Not safe to acquire a new lock, so run later in read access.
303
RP.post(new Runnable() {
305
ProjectManager.mutex().readAccess(action);
311
public synchronized void addChangeListener(ChangeListener l) {
312
cs.addChangeListener(l);
315
public synchronized void removeChangeListener(ChangeListener l) {
316
cs.removeChangeListener(l);
319
public void fileCreated(FileChangeSupportEvent event) {
320
//System.err.println("FPP: " + event);
324
public void fileDeleted(FileChangeSupportEvent event) {
325
//System.err.println("FPP: " + event);
329
public void fileModified(FileChangeSupportEvent event) {
330
//System.err.println("FPP: " + event);
334
public String toString() {
335
return "FilePropertyProvider[" + properties + ":" + getProperties() + "]"; // NOI18N
341
* Evaluate all properties in a list of property mappings.
343
* If there are any cyclic definitions within a single mapping,
344
* the evaluation will fail and return null.
345
* @param defs an ordered list of property mappings, e.g. {@link EditableProperties} instances
346
* @param predefs an unevaluated set of initial definitions
347
* @return values for all defined properties, or null if a circularity error was detected
349
private static Map<String,String> evaluateAll(Map<String,String> predefs, List<Map<String,String>> defs) {
350
Map<String,String> m = new HashMap<String,String>(predefs);
351
for (Map<String,String> curr : defs) {
352
// Set of properties which we are deferring because they subst sibling properties:
353
Map<String,Set<String>> dependOnSiblings = new HashMap<String,Set<String>>();
354
for (Map.Entry<String,String> entry : curr.entrySet()) {
355
String prop = entry.getKey();
356
if (!m.containsKey(prop)) {
357
String rawval = entry.getValue();
358
//System.err.println("subst " + prop + "=" + rawval + " with " + m);
359
Union2<String,Set<String>> o = substitute(rawval, m, curr.keySet());
361
m.put(prop, o.first());
363
dependOnSiblings.put(prop, o.second());
367
Set<String> toSort = new HashSet<String>(dependOnSiblings.keySet());
368
for (Set<String> s : dependOnSiblings.values()) {
373
sorted = Utilities.topologicalSort(toSort, dependOnSiblings);
374
} catch (TopologicalSortException e) {
375
//System.err.println("Cyclic property refs: " + Arrays.asList(e.unsortableSets()));
378
Collections.reverse(sorted);
379
for (String prop : sorted) {
380
if (!m.containsKey(prop)) {
381
String rawval = curr.get(prop);
382
m.put(prop, substitute(rawval, m, /*Collections.EMPTY_SET*/curr.keySet()).first());
390
* Try to substitute property references etc. in an Ant property value string.
391
* @param rawval the raw value to be substituted
392
* @param predefs a set of properties already defined
393
* @param siblingProperties a set of property names that are yet to be defined
394
* @return either a String, in case everything can be evaluated now;
395
* or a Set<String> of elements from siblingProperties in case those properties
396
* need to be defined in order to evaluate this one
398
private static Union2<String,Set<String>> substitute(String rawval, Map<String,String> predefs, Set<String> siblingProperties) {
399
assert rawval != null : "null rawval passed in";
400
if (rawval.indexOf('$') == -1) {
402
//System.err.println("shortcut");
403
return Union2.createFirst(rawval);
405
// May need to subst something.
407
// Result in progress, if it is to be a String:
408
StringBuffer val = new StringBuffer();
409
// Or, result in progress, if it is to be a Set<String>:
410
Set<String> needed = new HashSet<String>();
412
int shell = rawval.indexOf('$', idx);
413
if (shell == -1 || shell == rawval.length() - 1) {
414
// No more $, or only as last char -> copy all.
415
//System.err.println("no more $");
416
if (needed.isEmpty()) {
417
val.append(rawval.substring(idx));
418
return Union2.createFirst(val.toString());
420
return Union2.createSecond(needed);
423
char c = rawval.charAt(shell + 1);
426
//System.err.println("$$");
427
if (needed.isEmpty()) {
431
} else if (c == '{') {
432
// Possibly a property ref.
433
int end = rawval.indexOf('}', shell + 2);
435
// Definitely a property ref.
436
String otherprop = rawval.substring(shell + 2, end);
437
//System.err.println("prop ref to " + otherprop);
438
if (predefs.containsKey(otherprop)) {
440
if (needed.isEmpty()) {
441
val.append(rawval.substring(idx, shell));
442
val.append(predefs.get(otherprop));
445
} else if (siblingProperties.contains(otherprop)) {
446
needed.add(otherprop);
447
// don't bother updating val, it will not be used anyway
450
// No def, leave as is.
451
if (needed.isEmpty()) {
452
val.append(rawval.substring(idx, end + 1));
457
// Unclosed ${ sequence, leave as is.
458
if (needed.isEmpty()) {
459
val.append(rawval.substring(idx));
460
return Union2.createFirst(val.toString());
462
return Union2.createSecond(needed);
466
// $ followed by some other char, leave as is.
467
// XXX is this actually right?
468
if (needed.isEmpty()) {
469
val.append(rawval.substring(idx, idx + 2));
476
private static final Pattern RELATIVE_SLASH_SEPARATED_PATH = Pattern.compile("[^:/\\\\.][^:/\\\\]*(/[^:/\\\\.][^:/\\\\]*)*"); // NOI18N
479
* Find an absolute file path from a possibly relative path.
480
* @param basedir base file for relative filename resolving; must be an absolute path
481
* @param filename a pathname which may be relative or absolute and may
482
* use / or \ as the path separator
483
* @return an absolute file corresponding to it
484
* @throws IllegalArgumentException if basedir is not absolute
486
public static File resolveFile(File basedir, String filename) throws IllegalArgumentException {
487
if (basedir == null) {
488
throw new NullPointerException("null basedir passed to resolveFile"); // NOI18N
490
if (filename == null) {
491
throw new NullPointerException("null filename passed to resolveFile"); // NOI18N
493
if (!basedir.isAbsolute()) {
494
throw new IllegalArgumentException("nonabsolute basedir passed to resolveFile: " + basedir); // NOI18N
497
if (RELATIVE_SLASH_SEPARATED_PATH.matcher(filename).matches()) {
498
// Shortcut - simple relative path. Potentially faster.
499
f = new File(basedir, filename.replace('/', File.separatorChar));
502
String machinePath = filename.replace('/', File.separatorChar).replace('\\', File.separatorChar);
503
f = new File(machinePath);
504
if (!f.isAbsolute()) {
505
f = new File(basedir, machinePath);
507
assert f.isAbsolute();
509
return FileUtil.normalizeFile(f);
513
* Produce a machine-independent relativized version of a filename from a basedir.
514
* Unlike {@link URI#relativize} this will produce "../" sequences as needed.
515
* @param basedir a directory to resolve relative to (need not exist on disk)
516
* @param file a file or directory to find a relative path for
517
* @return a relativized path (slash-separated), or null if it is not possible (e.g. different DOS drives);
518
* just <samp>.</samp> in case the paths are the same
519
* @throws IllegalArgumentException if the basedir is known to be a file and not a directory
521
public static String relativizeFile(File basedir, File file) {
522
if (basedir.isFile()) {
523
throw new IllegalArgumentException("Cannot relative w.r.t. a data file " + basedir); // NOI18N
525
if (basedir.equals(file)) {
526
return "."; // NOI18N
528
StringBuffer b = new StringBuffer();
530
String filepath = file.getAbsolutePath();
531
while (!filepath.startsWith(slashify(base.getAbsolutePath()))) {
532
base = base.getParentFile();
536
if (base.equals(file)) {
537
// #61687: file is a parent of basedir
538
b.append(".."); // NOI18N
541
b.append("../"); // NOI18N
543
URI u = base.toURI().relativize(file.toURI());
544
assert !u.isAbsolute() : u + " from " + basedir + " and " + file + " with common root " + base;
545
b.append(u.getPath());
546
if (b.charAt(b.length() - 1) == '/') {
547
// file is an existing directory and file.toURI ends in /
548
// we do not want the trailing slash
549
b.setLength(b.length() - 1);
554
private static String slashify(String path) {
555
if (path.endsWith(File.separator)) {
558
return path + File.separatorChar;
562
/*public? */ static FileObject resolveFileObject(FileObject basedir, String filename) {
563
if (RELATIVE_SLASH_SEPARATED_PATH.matcher(filename).matches()) {
564
// Shortcut. Potentially much faster.
565
return basedir.getFileObject(filename);
567
// Might be an absolute path, or \-separated, or . or .. components, etc.; use the safer method.
568
return FileUtil.toFileObject(resolveFile(FileUtil.toFile(basedir), filename));
572
/*public? */ static String resolvePath(File basedir, String path) {
573
StringBuffer b = new StringBuffer();
574
String[] toks = tokenizePath(path);
575
for (int i = 0; i < toks.length; i++) {
577
b.append(File.pathSeparatorChar);
579
b.append(resolveFile(basedir, toks[i]).getAbsolutePath());
585
* Split an Ant-style path specification into components.
586
* Tokenizes on <code>:</code> and <code>;</code>, paying
587
* attention to DOS-style components such as <samp>C:\FOO</samp>.
588
* Also removes any empty components.
589
* @param path an Ant-style path (elements arbitrary) using DOS or Unix separators
590
* @return a tokenization of that path into components
592
public static String[] tokenizePath(String path) {
593
List<String> l = new ArrayList<String>();
594
StringTokenizer tok = new StringTokenizer(path, ":;", true); // NOI18N
596
char lastDelim = '\0';
598
while (tok.hasMoreTokens()) {
599
String s = tok.nextToken();
600
if (s.length() == 0) {
601
// Strip empty components.
604
if (s.length() == 1) {
605
char c = s.charAt(0);
606
if (c == ':' || c == ';') {
613
if (dosHack != '\0') {
614
// #50679 - "C:/something" is also accepted as DOS path
615
if (lastDelim == ':' && delimCount == 1 && (s.charAt(0) == '\\' || s.charAt(0) == '/')) {
616
// We had a single letter followed by ':' now followed by \something or /something
617
s = "" + dosHack + ':' + s;
618
// and use the new token with the drive prefix...
620
// Something else, leave alone.
621
l.add(Character.toString(dosHack));
622
// and continue with this token too...
626
// Reset count of # of delimiters in a row.
628
if (s.length() == 1) {
629
char c = s.charAt(0);
630
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
631
// Probably a DOS drive letter. Leave it with the next component.
638
if (dosHack != '\0') {
639
//the dosHack was the last letter in the input string (not followed by the ':')
640
//so obviously not a drive letter.
641
//Fix for issue #57304
642
l.add(Character.toString(dosHack));
644
return l.toArray(new String[l.size()]);
647
private static final Pattern VALID_PROPERTY_NAME = Pattern.compile("[-._a-zA-Z0-9]+"); // NOI18N
650
* Checks whether the name is usable as Ant property name.
651
* @param name name to check for usability as Ant property
652
* @return true if name is usable otherwise false
654
public static boolean isUsablePropertyName(String name) {
655
return VALID_PROPERTY_NAME.matcher(name).matches();
659
* Returns name usable as Ant property which is based on the given
660
* name. All forbidden characters are either removed or replaced with
662
* @param name name to use as base for Ant property name
663
* @return name usable as Ant property name
665
public static String getUsablePropertyName(String name) {
666
if (isUsablePropertyName(name)) {
669
StringBuffer sb = new StringBuffer(name);
670
for (int i=0; i<sb.length(); i++) {
671
if (!isUsablePropertyName(sb.substring(i,i+1))) {
672
sb.replace(i,i+1,"_");
675
return sb.toString();
679
* Create a trivial property producer using only a fixed list of property definitions.
680
* Its values are constant, and it never fires changes.
681
* @param defs a map from property names to values (it is illegal to modify this map
682
* after passing it to this method)
683
* @return a matching property producer
685
public static PropertyProvider fixedPropertyProvider(Map<String,String> defs) {
686
return new FixedPropertyProvider(defs);
689
private static final class FixedPropertyProvider implements PropertyProvider {
691
private final Map<String,String> defs;
693
public FixedPropertyProvider(Map<String,String> defs) {
697
public Map<String,String> getProperties() {
701
public void addChangeListener(ChangeListener l) {}
703
public void removeChangeListener(ChangeListener l) {}
708
* Create a property evaluator based on a series of definitions.
710
* Each batch of definitions can refer to properties within itself
711
* (so long as there is no cycle) or any previous batch.
712
* However the special first provider cannot refer to properties within itself.
715
* This implementation acquires {@link ProjectManager#mutex} for all operations, in read mode,
716
* and fires changes synchronously. It also expects changes to be fired from property
717
* providers in read (or write) access.
719
* @param preprovider an initial context (may be null)
720
* @param providers a sequential list of property groups
721
* @return an evaluator
723
public static PropertyEvaluator sequentialPropertyEvaluator(PropertyProvider preprovider, PropertyProvider... providers) {
724
return new SequentialPropertyEvaluator(preprovider, providers);
728
* Creates a property provider similar to {@link #globalPropertyProvider}
729
* but which can use a different global properties file.
730
* If a specific file is pointed to, that is loaded; otherwise behaves like {@link #globalPropertyProvider}.
731
* Permits behavior similar to command-line Ant where not erroneous, but using the IDE's
732
* default global properties for projects which do not yet have this property registered.
733
* @param findUserPropertiesFile an evaluator in which to look up <code>propertyName</code>
734
* @param propertyName a property pointing to the global properties file (typically <code>"user.properties.file"</code>)
735
* @param basedir a base directory to use when resolving the path to the global properties file, if relative
736
* @return a provider of global properties
737
* @since org.netbeans.modules.project.ant/1 1.14
739
public static PropertyProvider userPropertiesProvider(PropertyEvaluator findUserPropertiesFile, String propertyName, File basedir) {
740
return new UserPropertiesProvider(findUserPropertiesFile, propertyName, basedir);
742
private static final class UserPropertiesProvider extends FilterPropertyProvider implements PropertyChangeListener {
743
private final PropertyEvaluator findUserPropertiesFile;
744
private final String propertyName;
745
private final File basedir;
746
public UserPropertiesProvider(PropertyEvaluator findUserPropertiesFile, String propertyName, File basedir) {
747
super(computeDelegate(findUserPropertiesFile, propertyName, basedir));
748
this.findUserPropertiesFile = findUserPropertiesFile;
749
this.propertyName = propertyName;
750
this.basedir = basedir;
751
findUserPropertiesFile.addPropertyChangeListener(this);
753
public void propertyChange(PropertyChangeEvent ev) {
754
if (propertyName.equals(ev.getPropertyName())) {
755
setDelegate(computeDelegate(findUserPropertiesFile, propertyName, basedir));
758
private static PropertyProvider computeDelegate(PropertyEvaluator findUserPropertiesFile, String propertyName, File basedir) {
759
String userPropertiesFile = findUserPropertiesFile.getProperty(propertyName);
760
if (userPropertiesFile != null) {
761
// Have some defined global properties file, so read it and listen to changes in it.
762
File f = PropertyUtils.resolveFile(basedir, userPropertiesFile);
763
if (f.equals(PropertyUtils.userBuildProperties())) {
764
// Just to share the cache.
765
return PropertyUtils.globalPropertyProvider();
767
return PropertyUtils.propertiesFilePropertyProvider(f);
770
// Use the in-IDE default.
771
return PropertyUtils.globalPropertyProvider();
776
private static final class SequentialPropertyEvaluator implements PropertyEvaluator, ChangeListener {
778
private final PropertyProvider preprovider;
779
private final PropertyProvider[] providers;
780
private Map<String,String> defs;
781
private final List<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
783
public SequentialPropertyEvaluator(final PropertyProvider preprovider, final PropertyProvider[] providers) {
784
this.preprovider = preprovider;
785
this.providers = providers;
786
// XXX defer until someone asks for them
787
defs = ProjectManager.mutex().readAccess(new Mutex.Action<Map<String,String>>() {
788
public Map<String,String> run() {
789
return compose(preprovider, providers);
792
// XXX defer until someone is listening?
793
if (preprovider != null) {
794
preprovider.addChangeListener(WeakListeners.change(this, preprovider));
796
for (PropertyProvider pp : providers) {
797
pp.addChangeListener(WeakListeners.change(this, pp));
801
public String getProperty(final String prop) {
802
return ProjectManager.mutex().readAccess(new Mutex.Action<String>() {
803
public String run() {
807
return defs.get(prop);
812
public String evaluate(final String text) {
814
throw new NullPointerException("Attempted to pass null to PropertyEvaluator.evaluate"); // NOI18N
816
return ProjectManager.mutex().readAccess(new Mutex.Action<String>() {
817
public String run() {
821
Union2<String,Set<String>> result = substitute(text, defs, Collections.<String>emptySet());
822
assert result.hasFirst() : "Unexpected result " + result + " from " + text + " on " + defs;
823
return result.first();
828
public Map<String,String> getProperties() {
829
return ProjectManager.mutex().readAccess(new Mutex.Action<Map<String,String>>() {
830
public Map<String,String> run() {
836
public void addPropertyChangeListener(PropertyChangeListener listener) {
837
synchronized (listeners) {
838
listeners.add(listener);
842
public void removePropertyChangeListener(PropertyChangeListener listener) {
843
synchronized (listeners) {
844
listeners.remove(listener);
848
public void stateChanged(ChangeEvent e) {
849
assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
850
Map<String,String> newdefs = compose(preprovider, providers);
851
// compose() may return null upon circularity errors
852
Map<String,String> _defs = defs != null ? defs : Collections.<String,String>emptyMap();
853
Map<String,String> _newdefs = newdefs != null ? newdefs : Collections.<String,String>emptyMap();
854
if (!_defs.equals(_newdefs)) {
855
Set<String> props = new HashSet<String>(_defs.keySet());
856
props.addAll(_newdefs.keySet());
857
List<PropertyChangeEvent> events = new LinkedList<PropertyChangeEvent>();
858
for (String prop : props) {
860
String oldval = _defs.get(prop);
861
String newval = _newdefs.get(prop);
862
if (newval != null) {
863
if (newval.equals(oldval)) {
867
assert oldval != null : "should not have had " + prop;
869
events.add(new PropertyChangeEvent(this, prop, oldval, newval));
871
assert !events.isEmpty();
873
PropertyChangeListener[] _listeners;
874
synchronized (listeners) {
875
_listeners = listeners.toArray(new PropertyChangeListener[listeners.size()]);
877
for (PropertyChangeListener l : _listeners) {
878
for (PropertyChangeEvent ev : events) {
879
l.propertyChange(ev);
885
private static Map<String,String> compose(PropertyProvider preprovider, PropertyProvider[] providers) {
886
assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
887
Map<String,String> predefs;
888
if (preprovider != null) {
889
predefs = preprovider.getProperties();
891
predefs = Collections.emptyMap();
893
List<Map<String,String>> defs = new ArrayList<Map<String,String>>(providers.length);
894
for (PropertyProvider pp : providers) {
895
defs.add(pp.getProperties());
897
return evaluateAll(predefs, defs);