0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.netbeans.spi.project.support.ant;
0043:
0044: import java.beans.PropertyChangeEvent;
0045: import java.beans.PropertyChangeListener;
0046: import java.io.File;
0047: import java.io.FileInputStream;
0048: import java.io.FileOutputStream;
0049: import java.io.IOException;
0050: import java.io.InputStream;
0051: import java.io.OutputStream;
0052: import java.lang.ref.Reference;
0053: import java.lang.ref.SoftReference;
0054: import java.net.URI;
0055: import java.util.ArrayList;
0056: import java.util.Collections;
0057: import java.util.HashMap;
0058: import java.util.HashSet;
0059: import java.util.LinkedList;
0060: import java.util.List;
0061: import java.util.Map;
0062: import java.util.Properties;
0063: import java.util.Set;
0064: import java.util.StringTokenizer;
0065: import java.util.logging.Level;
0066: import java.util.logging.Logger;
0067: import java.util.regex.Pattern;
0068: import javax.swing.event.ChangeEvent;
0069: import javax.swing.event.ChangeListener;
0070: import org.netbeans.api.project.ProjectManager;
0071: import org.netbeans.modules.project.ant.FileChangeSupport;
0072: import org.netbeans.modules.project.ant.FileChangeSupportEvent;
0073: import org.netbeans.modules.project.ant.FileChangeSupportListener;
0074: import org.openide.ErrorManager;
0075: import org.openide.filesystems.FileLock;
0076: import org.openide.filesystems.FileObject;
0077: import org.openide.filesystems.FileUtil;
0078: import org.openide.util.ChangeSupport;
0079: import org.openide.util.Mutex;
0080: import org.openide.util.MutexException;
0081: import org.openide.util.NbCollections;
0082: import org.openide.util.RequestProcessor;
0083: import org.openide.util.TopologicalSortException;
0084: import org.openide.util.Union2;
0085: import org.openide.util.Utilities;
0086: import org.openide.util.WeakListeners;
0087:
0088: /**
0089: * Support for working with Ant properties and property files.
0090: * @author Jesse Glick
0091: */
0092: public class PropertyUtils {
0093:
0094: private PropertyUtils() {
0095: }
0096:
0097: /**
0098: * Location in user directory of per-user global properties.
0099: * May be null if <code>netbeans.user</code> is not set.
0100: */
0101: static File userBuildProperties() {
0102: String nbuser = System.getProperty("netbeans.user"); // NOI18N
0103: if (nbuser != null) {
0104: return FileUtil.normalizeFile(new File(nbuser,
0105: "build.properties")); // NOI18N
0106: } else {
0107: return null;
0108: }
0109: }
0110:
0111: private static Map<File, Reference<PropertyProvider>> globalPropertyProviders = new HashMap<File, Reference<PropertyProvider>>();
0112:
0113: /**
0114: * Load global properties defined by the IDE in the user directory.
0115: * Currently loads ${netbeans.user}/build.properties if it exists.
0116: * <p>
0117: * Acquires read access.
0118: * <p>
0119: * To listen to changes use {@link #globalPropertyProvider}.
0120: * @return user properties (empty if missing or malformed)
0121: */
0122: public static EditableProperties getGlobalProperties() {
0123: return ProjectManager.mutex().readAccess(
0124: new Mutex.Action<EditableProperties>() {
0125: public EditableProperties run() {
0126: File ubp = userBuildProperties();
0127: if (ubp != null && ubp.isFile()
0128: && ubp.canRead()) {
0129: try {
0130: InputStream is = new FileInputStream(
0131: ubp);
0132: try {
0133: EditableProperties properties = new EditableProperties(
0134: true);
0135: properties.load(is);
0136: return properties;
0137: } finally {
0138: is.close();
0139: }
0140: } catch (IOException e) {
0141: Logger.getLogger(
0142: PropertyUtils.class.getName())
0143: .log(Level.INFO, null, e);
0144: }
0145: }
0146: // Missing or erroneous.
0147: return new EditableProperties(true);
0148: }
0149: });
0150: }
0151:
0152: /**
0153: * Edit global properties defined by the IDE in the user directory.
0154: * <p>
0155: * Acquires write access.
0156: * @param properties user properties to set
0157: * @throws IOException if they could not be stored
0158: * @see #getGlobalProperties
0159: */
0160: public static void putGlobalProperties(
0161: final EditableProperties properties) throws IOException {
0162: try {
0163: ProjectManager.mutex().writeAccess(
0164: new Mutex.ExceptionAction<Void>() {
0165: public Void run() throws IOException {
0166: File ubp = userBuildProperties();
0167: if (ubp != null) {
0168: FileObject bp = FileUtil
0169: .toFileObject(ubp);
0170: if (bp == null) {
0171: if (!ubp.exists()) {
0172: ubp.getParentFile().mkdirs();
0173: new FileOutputStream(ubp)
0174: .close();
0175: assert ubp.isFile() : "Did not actually make "
0176: + ubp;
0177: }
0178: bp = FileUtil.toFileObject(ubp);
0179: if (bp == null) {
0180: // XXX ugly (and will not correctly notify changes) but better than nothing:
0181: ErrorManager
0182: .getDefault()
0183: .log(
0184: ErrorManager.WARNING,
0185: "Warning - cannot properly write to "
0186: + ubp
0187: + "; might be because your user directory is on a Windows UNC path (issue #46813)? If so, try using mapped drive letters.");
0188: OutputStream os = new FileOutputStream(
0189: ubp);
0190: try {
0191: properties.store(os);
0192: } finally {
0193: os.close();
0194: }
0195: return null;
0196: }
0197: }
0198: FileLock lock = bp.lock();
0199: try {
0200: OutputStream os = bp
0201: .getOutputStream(lock);
0202: try {
0203: properties.store(os);
0204: } finally {
0205: os.close();
0206: }
0207: } finally {
0208: lock.releaseLock();
0209: }
0210: } else {
0211: throw new IOException(
0212: "Do not know where to store build.properties; must set netbeans.user!"); // NOI18N
0213: }
0214: return null;
0215: }
0216: });
0217: } catch (MutexException e) {
0218: throw (IOException) e.getException();
0219: }
0220: }
0221:
0222: /**
0223: * Create a property evaluator based on {@link #getGlobalProperties}
0224: * and {@link #putGlobalProperties}.
0225: * It will supply global properties and fire changes when this file
0226: * is changed.
0227: * @return a property producer
0228: */
0229: public static synchronized PropertyProvider globalPropertyProvider() {
0230: File ubp = userBuildProperties();
0231: if (ubp != null) {
0232: Reference<PropertyProvider> globalPropertyProvider = globalPropertyProviders
0233: .get(ubp);
0234: if (globalPropertyProvider != null) {
0235: PropertyProvider pp = globalPropertyProvider.get();
0236: if (pp != null) {
0237: return pp;
0238: }
0239: }
0240: PropertyProvider gpp = propertiesFilePropertyProvider(ubp);
0241: globalPropertyProviders.put(ubp,
0242: new SoftReference<PropertyProvider>(gpp));
0243: return gpp;
0244: } else {
0245: return fixedPropertyProvider(Collections
0246: .<String, String> emptyMap());
0247: }
0248: }
0249:
0250: /**
0251: * Create a property provider based on a properties file.
0252: * The file need not exist at the moment; if it is created or deleted an appropriate
0253: * change will be fired. If its contents are changed on disk a change will also be fired.
0254: * @param propertiesFile a path to a (possibly nonexistent) *.properties file
0255: * @return a supplier of properties from such a file
0256: * @see Properties#load
0257: */
0258: public static PropertyProvider propertiesFilePropertyProvider(
0259: File propertiesFile) {
0260: assert propertiesFile != null;
0261: return new FilePropertyProvider(propertiesFile);
0262: }
0263:
0264: /**
0265: * Provider based on a named properties file.
0266: */
0267: private static final class FilePropertyProvider implements
0268: PropertyProvider, FileChangeSupportListener {
0269:
0270: private static final RequestProcessor RP = new RequestProcessor(
0271: "PropertyUtils.FilePropertyProvider.RP"); // NOI18N
0272:
0273: private final File properties;
0274: private final ChangeSupport cs = new ChangeSupport(this );
0275: private Map<String, String> cached = null;
0276: private long cachedTime = 0L;
0277:
0278: public FilePropertyProvider(File properties) {
0279: this .properties = properties;
0280: FileChangeSupport.DEFAULT.addListener(this , properties);
0281: }
0282:
0283: public Map<String, String> getProperties() {
0284: long currTime = properties.lastModified();
0285: if (cached == null || cachedTime != currTime) {
0286: cachedTime = currTime;
0287: cached = loadProperties();
0288: }
0289: return cached;
0290: }
0291:
0292: private Map<String, String> loadProperties() {
0293: // XXX does this need to run in PM.mutex.readAccess?
0294: if (properties.isFile() && properties.canRead()) {
0295: try {
0296: InputStream is = new FileInputStream(properties);
0297: try {
0298: Properties props = new Properties();
0299: props.load(is);
0300: return NbCollections.checkedMapByFilter(props,
0301: String.class, String.class, true);
0302: } finally {
0303: is.close();
0304: }
0305: } catch (IOException e) {
0306: Logger.getLogger(PropertyUtils.class.getName())
0307: .log(Level.INFO, null, e);
0308: }
0309: }
0310: // Missing or erroneous.
0311: return Collections.emptyMap();
0312: }
0313:
0314: private void fireChange() {
0315: cachedTime = -1L; // force reload
0316: if (!cs.hasListeners()) {
0317: return;
0318: }
0319: final Mutex.Action<Void> action = new Mutex.Action<Void>() {
0320: public Void run() {
0321: cs.fireChange();
0322: return null;
0323: }
0324: };
0325: if (ProjectManager.mutex().isWriteAccess()) {
0326: // Run it right now. postReadRequest would be too late.
0327: ProjectManager.mutex().readAccess(action);
0328: } else if (ProjectManager.mutex().isReadAccess()) {
0329: // Run immediately also. No need to switch to read access.
0330: action.run();
0331: } else {
0332: // Not safe to acquire a new lock, so run later in read access.
0333: RP.post(new Runnable() {
0334: public void run() {
0335: ProjectManager.mutex().readAccess(action);
0336: }
0337: });
0338: }
0339: }
0340:
0341: public synchronized void addChangeListener(ChangeListener l) {
0342: cs.addChangeListener(l);
0343: }
0344:
0345: public synchronized void removeChangeListener(ChangeListener l) {
0346: cs.removeChangeListener(l);
0347: }
0348:
0349: public void fileCreated(FileChangeSupportEvent event) {
0350: //System.err.println("FPP: " + event);
0351: fireChange();
0352: }
0353:
0354: public void fileDeleted(FileChangeSupportEvent event) {
0355: //System.err.println("FPP: " + event);
0356: fireChange();
0357: }
0358:
0359: public void fileModified(FileChangeSupportEvent event) {
0360: //System.err.println("FPP: " + event);
0361: fireChange();
0362: }
0363:
0364: public String toString() {
0365: return "FilePropertyProvider[" + properties + ":"
0366: + getProperties() + "]"; // NOI18N
0367: }
0368:
0369: }
0370:
0371: /**
0372: * Evaluate all properties in a list of property mappings.
0373: * <p>
0374: * If there are any cyclic definitions within a single mapping,
0375: * the evaluation will fail and return null.
0376: * @param defs an ordered list of property mappings, e.g. {@link EditableProperties} instances
0377: * @param predefs an unevaluated set of initial definitions
0378: * @return values for all defined properties, or null if a circularity error was detected
0379: */
0380: private static Map<String, String> evaluateAll(
0381: Map<String, String> predefs, List<Map<String, String>> defs) {
0382: Map<String, String> m = new HashMap<String, String>(predefs);
0383: for (Map<String, String> curr : defs) {
0384: // Set of properties which we are deferring because they subst sibling properties:
0385: Map<String, Set<String>> dependOnSiblings = new HashMap<String, Set<String>>();
0386: for (Map.Entry<String, String> entry : curr.entrySet()) {
0387: String prop = entry.getKey();
0388: if (!m.containsKey(prop)) {
0389: String rawval = entry.getValue();
0390: //System.err.println("subst " + prop + "=" + rawval + " with " + m);
0391: Union2<String, Set<String>> o = substitute(rawval,
0392: m, curr.keySet());
0393: if (o.hasFirst()) {
0394: m.put(prop, o.first());
0395: } else {
0396: dependOnSiblings.put(prop, o.second());
0397: }
0398: }
0399: }
0400: Set<String> toSort = new HashSet<String>(dependOnSiblings
0401: .keySet());
0402: for (Set<String> s : dependOnSiblings.values()) {
0403: toSort.addAll(s);
0404: }
0405: List<String> sorted;
0406: try {
0407: sorted = Utilities.topologicalSort(toSort,
0408: dependOnSiblings);
0409: } catch (TopologicalSortException e) {
0410: //System.err.println("Cyclic property refs: " + Arrays.asList(e.unsortableSets()));
0411: return null;
0412: }
0413: Collections.reverse(sorted);
0414: for (String prop : sorted) {
0415: if (!m.containsKey(prop)) {
0416: String rawval = curr.get(prop);
0417: m.put(prop, substitute(rawval, m, /*Collections.EMPTY_SET*/
0418: curr.keySet()).first());
0419: }
0420: }
0421: }
0422: return m;
0423: }
0424:
0425: /**
0426: * Try to substitute property references etc. in an Ant property value string.
0427: * @param rawval the raw value to be substituted
0428: * @param predefs a set of properties already defined
0429: * @param siblingProperties a set of property names that are yet to be defined
0430: * @return either a String, in case everything can be evaluated now;
0431: * or a Set<String> of elements from siblingProperties in case those properties
0432: * need to be defined in order to evaluate this one
0433: */
0434: private static Union2<String, Set<String>> substitute(
0435: String rawval, Map<String, String> predefs,
0436: Set<String> siblingProperties) {
0437: assert rawval != null : "null rawval passed in";
0438: if (rawval.indexOf('$') == -1) {
0439: // Shortcut:
0440: //System.err.println("shortcut");
0441: return Union2.createFirst(rawval);
0442: }
0443: // May need to subst something.
0444: int idx = 0;
0445: // Result in progress, if it is to be a String:
0446: StringBuffer val = new StringBuffer();
0447: // Or, result in progress, if it is to be a Set<String>:
0448: Set<String> needed = new HashSet<String>();
0449: while (true) {
0450: int shell = rawval.indexOf('$', idx);
0451: if (shell == -1 || shell == rawval.length() - 1) {
0452: // No more $, or only as last char -> copy all.
0453: //System.err.println("no more $");
0454: if (needed.isEmpty()) {
0455: val.append(rawval.substring(idx));
0456: return Union2.createFirst(val.toString());
0457: } else {
0458: return Union2.createSecond(needed);
0459: }
0460: }
0461: char c = rawval.charAt(shell + 1);
0462: if (c == '$') {
0463: // $$ -> $
0464: //System.err.println("$$");
0465: if (needed.isEmpty()) {
0466: val.append('$');
0467: }
0468: idx += 2;
0469: } else if (c == '{') {
0470: // Possibly a property ref.
0471: int end = rawval.indexOf('}', shell + 2);
0472: if (end != -1) {
0473: // Definitely a property ref.
0474: String otherprop = rawval.substring(shell + 2, end);
0475: //System.err.println("prop ref to " + otherprop);
0476: if (predefs.containsKey(otherprop)) {
0477: // Well-defined.
0478: if (needed.isEmpty()) {
0479: val.append(rawval.substring(idx, shell));
0480: val.append(predefs.get(otherprop));
0481: }
0482: idx = end + 1;
0483: } else if (siblingProperties.contains(otherprop)) {
0484: needed.add(otherprop);
0485: // don't bother updating val, it will not be used anyway
0486: idx = end + 1;
0487: } else {
0488: // No def, leave as is.
0489: if (needed.isEmpty()) {
0490: val.append(rawval.substring(idx, end + 1));
0491: }
0492: idx = end + 1;
0493: }
0494: } else {
0495: // Unclosed ${ sequence, leave as is.
0496: if (needed.isEmpty()) {
0497: val.append(rawval.substring(idx));
0498: return Union2.createFirst(val.toString());
0499: } else {
0500: return Union2.createSecond(needed);
0501: }
0502: }
0503: } else {
0504: // $ followed by some other char, leave as is.
0505: // XXX is this actually right?
0506: if (needed.isEmpty()) {
0507: val.append(rawval.substring(idx, idx + 2));
0508: }
0509: idx += 2;
0510: }
0511: }
0512: }
0513:
0514: private static final Pattern RELATIVE_SLASH_SEPARATED_PATH = Pattern
0515: .compile("[^:/\\\\.][^:/\\\\]*(/[^:/\\\\.][^:/\\\\]*)*"); // NOI18N
0516:
0517: /**
0518: * Find an absolute file path from a possibly relative path.
0519: * @param basedir base file for relative filename resolving; must be an absolute path
0520: * @param filename a pathname which may be relative or absolute and may
0521: * use / or \ as the path separator
0522: * @return an absolute file corresponding to it
0523: * @throws IllegalArgumentException if basedir is not absolute
0524: */
0525: public static File resolveFile(File basedir, String filename)
0526: throws IllegalArgumentException {
0527: if (basedir == null) {
0528: throw new NullPointerException(
0529: "null basedir passed to resolveFile"); // NOI18N
0530: }
0531: if (filename == null) {
0532: throw new NullPointerException(
0533: "null filename passed to resolveFile"); // NOI18N
0534: }
0535: if (!basedir.isAbsolute()) {
0536: throw new IllegalArgumentException(
0537: "nonabsolute basedir passed to resolveFile: "
0538: + basedir); // NOI18N
0539: }
0540: File f;
0541: if (RELATIVE_SLASH_SEPARATED_PATH.matcher(filename).matches()) {
0542: // Shortcut - simple relative path. Potentially faster.
0543: f = new File(basedir, filename.replace('/',
0544: File.separatorChar));
0545: } else {
0546: // All other cases.
0547: String machinePath = filename.replace('/',
0548: File.separatorChar).replace('\\',
0549: File.separatorChar);
0550: f = new File(machinePath);
0551: if (!f.isAbsolute()) {
0552: f = new File(basedir, machinePath);
0553: }
0554: assert f.isAbsolute();
0555: }
0556: return FileUtil.normalizeFile(f);
0557: }
0558:
0559: /**
0560: * Produce a machine-independent relativized version of a filename from a basedir.
0561: * Unlike {@link URI#relativize} this will produce "../" sequences as needed.
0562: * @param basedir a directory to resolve relative to (need not exist on disk)
0563: * @param file a file or directory to find a relative path for
0564: * @return a relativized path (slash-separated), or null if it is not possible (e.g. different DOS drives);
0565: * just <samp>.</samp> in case the paths are the same
0566: * @throws IllegalArgumentException if the basedir is known to be a file and not a directory
0567: */
0568: public static String relativizeFile(File basedir, File file) {
0569: if (basedir.isFile()) {
0570: throw new IllegalArgumentException(
0571: "Cannot relative w.r.t. a data file " + basedir); // NOI18N
0572: }
0573: if (basedir.equals(file)) {
0574: return "."; // NOI18N
0575: }
0576: StringBuffer b = new StringBuffer();
0577: File base = basedir;
0578: String filepath = file.getAbsolutePath();
0579: while (!filepath.startsWith(slashify(base.getAbsolutePath()))) {
0580: base = base.getParentFile();
0581: if (base == null) {
0582: return null;
0583: }
0584: if (base.equals(file)) {
0585: // #61687: file is a parent of basedir
0586: b.append(".."); // NOI18N
0587: return b.toString();
0588: }
0589: b.append("../"); // NOI18N
0590: }
0591: URI u = base.toURI().relativize(file.toURI());
0592: assert !u.isAbsolute() : u + " from " + basedir + " and "
0593: + file + " with common root " + base;
0594: b.append(u.getPath());
0595: if (b.charAt(b.length() - 1) == '/') {
0596: // file is an existing directory and file.toURI ends in /
0597: // we do not want the trailing slash
0598: b.setLength(b.length() - 1);
0599: }
0600: return b.toString();
0601: }
0602:
0603: private static String slashify(String path) {
0604: if (path.endsWith(File.separator)) {
0605: return path;
0606: } else {
0607: return path + File.separatorChar;
0608: }
0609: }
0610:
0611: /*public? */static FileObject resolveFileObject(
0612: FileObject basedir, String filename) {
0613: // an absolute path, or \-separated, or . or .. components, etc.; use the safer method.
0614: return FileUtil.toFileObject(resolveFile(FileUtil
0615: .toFile(basedir), filename));
0616: }
0617:
0618: /*public? */static String resolvePath(File basedir, String path) {
0619: StringBuffer b = new StringBuffer();
0620: String[] toks = tokenizePath(path);
0621: for (int i = 0; i < toks.length; i++) {
0622: if (i > 0) {
0623: b.append(File.pathSeparatorChar);
0624: }
0625: b.append(resolveFile(basedir, toks[i]).getAbsolutePath());
0626: }
0627: return b.toString();
0628: }
0629:
0630: /**
0631: * Split an Ant-style path specification into components.
0632: * Tokenizes on <code>:</code> and <code>;</code>, paying
0633: * attention to DOS-style components such as <samp>C:\FOO</samp>.
0634: * Also removes any empty components.
0635: * @param path an Ant-style path (elements arbitrary) using DOS or Unix separators
0636: * @return a tokenization of that path into components
0637: */
0638: public static String[] tokenizePath(String path) {
0639: List<String> l = new ArrayList<String>();
0640: StringTokenizer tok = new StringTokenizer(path, ":;", true); // NOI18N
0641: char dosHack = '\0';
0642: char lastDelim = '\0';
0643: int delimCount = 0;
0644: while (tok.hasMoreTokens()) {
0645: String s = tok.nextToken();
0646: if (s.length() == 0) {
0647: // Strip empty components.
0648: continue;
0649: }
0650: if (s.length() == 1) {
0651: char c = s.charAt(0);
0652: if (c == ':' || c == ';') {
0653: // Just a delimiter.
0654: lastDelim = c;
0655: delimCount++;
0656: continue;
0657: }
0658: }
0659: if (dosHack != '\0') {
0660: // #50679 - "C:/something" is also accepted as DOS path
0661: if (lastDelim == ':' && delimCount == 1
0662: && (s.charAt(0) == '\\' || s.charAt(0) == '/')) {
0663: // We had a single letter followed by ':' now followed by \something or /something
0664: s = "" + dosHack + ':' + s;
0665: // and use the new token with the drive prefix...
0666: } else {
0667: // Something else, leave alone.
0668: l.add(Character.toString(dosHack));
0669: // and continue with this token too...
0670: }
0671: dosHack = '\0';
0672: }
0673: // Reset count of # of delimiters in a row.
0674: delimCount = 0;
0675: if (s.length() == 1) {
0676: char c = s.charAt(0);
0677: if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
0678: // Probably a DOS drive letter. Leave it with the next component.
0679: dosHack = c;
0680: continue;
0681: }
0682: }
0683: l.add(s);
0684: }
0685: if (dosHack != '\0') {
0686: //the dosHack was the last letter in the input string (not followed by the ':')
0687: //so obviously not a drive letter.
0688: //Fix for issue #57304
0689: l.add(Character.toString(dosHack));
0690: }
0691: return l.toArray(new String[l.size()]);
0692: }
0693:
0694: private static final Pattern VALID_PROPERTY_NAME = Pattern
0695: .compile("[-._a-zA-Z0-9]+"); // NOI18N
0696:
0697: /**
0698: * Checks whether the name is usable as Ant property name.
0699: * @param name name to check for usability as Ant property
0700: * @return true if name is usable otherwise false
0701: */
0702: public static boolean isUsablePropertyName(String name) {
0703: return VALID_PROPERTY_NAME.matcher(name).matches();
0704: }
0705:
0706: /**
0707: * Returns name usable as Ant property which is based on the given
0708: * name. All forbidden characters are either removed or replaced with
0709: * suitable ones.
0710: * @param name name to use as base for Ant property name
0711: * @return name usable as Ant property name
0712: */
0713: public static String getUsablePropertyName(String name) {
0714: if (isUsablePropertyName(name)) {
0715: return name;
0716: }
0717: StringBuffer sb = new StringBuffer(name);
0718: for (int i = 0; i < sb.length(); i++) {
0719: if (!isUsablePropertyName(sb.substring(i, i + 1))) {
0720: sb.replace(i, i + 1, "_");
0721: }
0722: }
0723: return sb.toString();
0724: }
0725:
0726: /**
0727: * Create a trivial property producer using only a fixed list of property definitions.
0728: * Its values are constant, and it never fires changes.
0729: * @param defs a map from property names to values (it is illegal to modify this map
0730: * after passing it to this method)
0731: * @return a matching property producer
0732: */
0733: public static PropertyProvider fixedPropertyProvider(
0734: Map<String, String> defs) {
0735: return new FixedPropertyProvider(defs);
0736: }
0737:
0738: private static final class FixedPropertyProvider implements
0739: PropertyProvider {
0740:
0741: private final Map<String, String> defs;
0742:
0743: public FixedPropertyProvider(Map<String, String> defs) {
0744: this .defs = defs;
0745: }
0746:
0747: public Map<String, String> getProperties() {
0748: return defs;
0749: }
0750:
0751: public void addChangeListener(ChangeListener l) {
0752: }
0753:
0754: public void removeChangeListener(ChangeListener l) {
0755: }
0756:
0757: }
0758:
0759: /**
0760: * Create a property evaluator based on a series of definitions.
0761: * <p>
0762: * Each batch of definitions can refer to properties within itself
0763: * (so long as there is no cycle) or any previous batch.
0764: * However the special first provider cannot refer to properties within itself.
0765: * </p>
0766: * <p>
0767: * This implementation acquires {@link ProjectManager#mutex} for all operations, in read mode,
0768: * and fires changes synchronously. It also expects changes to be fired from property
0769: * providers in read (or write) access.
0770: * </p>
0771: * @param preprovider an initial context (may be null)
0772: * @param providers a sequential list of property groups
0773: * @return an evaluator
0774: */
0775: public static PropertyEvaluator sequentialPropertyEvaluator(
0776: PropertyProvider preprovider, PropertyProvider... providers) {
0777: return new SequentialPropertyEvaluator(preprovider, providers);
0778: }
0779:
0780: /**
0781: * Creates a property provider similar to {@link #globalPropertyProvider}
0782: * but which can use a different global properties file.
0783: * If a specific file is pointed to, that is loaded; otherwise behaves like {@link #globalPropertyProvider}.
0784: * Permits behavior similar to command-line Ant where not erroneous, but using the IDE's
0785: * default global properties for projects which do not yet have this property registered.
0786: * @param findUserPropertiesFile an evaluator in which to look up <code>propertyName</code>
0787: * @param propertyName a property pointing to the global properties file (typically <code>"user.properties.file"</code>)
0788: * @param basedir a base directory to use when resolving the path to the global properties file, if relative
0789: * @return a provider of global properties
0790: * @since org.netbeans.modules.project.ant/1 1.14
0791: */
0792: public static PropertyProvider userPropertiesProvider(
0793: PropertyEvaluator findUserPropertiesFile,
0794: String propertyName, File basedir) {
0795: return new UserPropertiesProvider(findUserPropertiesFile,
0796: propertyName, basedir);
0797: }
0798:
0799: private static final class UserPropertiesProvider extends
0800: FilterPropertyProvider implements PropertyChangeListener {
0801: private final PropertyEvaluator findUserPropertiesFile;
0802: private final String propertyName;
0803: private final File basedir;
0804:
0805: public UserPropertiesProvider(
0806: PropertyEvaluator findUserPropertiesFile,
0807: String propertyName, File basedir) {
0808: super (computeDelegate(findUserPropertiesFile, propertyName,
0809: basedir));
0810: this .findUserPropertiesFile = findUserPropertiesFile;
0811: this .propertyName = propertyName;
0812: this .basedir = basedir;
0813: findUserPropertiesFile.addPropertyChangeListener(this );
0814: }
0815:
0816: public void propertyChange(PropertyChangeEvent ev) {
0817: if (propertyName.equals(ev.getPropertyName())) {
0818: setDelegate(computeDelegate(findUserPropertiesFile,
0819: propertyName, basedir));
0820: }
0821: }
0822:
0823: private static PropertyProvider computeDelegate(
0824: PropertyEvaluator findUserPropertiesFile,
0825: String propertyName, File basedir) {
0826: String userPropertiesFile = findUserPropertiesFile
0827: .getProperty(propertyName);
0828: if (userPropertiesFile != null) {
0829: // Have some defined global properties file, so read it and listen to changes in it.
0830: File f = PropertyUtils.resolveFile(basedir,
0831: userPropertiesFile);
0832: if (f.equals(PropertyUtils.userBuildProperties())) {
0833: // Just to share the cache.
0834: return PropertyUtils.globalPropertyProvider();
0835: } else {
0836: return PropertyUtils
0837: .propertiesFilePropertyProvider(f);
0838: }
0839: } else {
0840: // Use the in-IDE default.
0841: return PropertyUtils.globalPropertyProvider();
0842: }
0843: }
0844: }
0845:
0846: private static final class SequentialPropertyEvaluator implements
0847: PropertyEvaluator, ChangeListener {
0848:
0849: private final PropertyProvider preprovider;
0850: private final PropertyProvider[] providers;
0851: private Map<String, String> defs;
0852: private final List<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
0853:
0854: public SequentialPropertyEvaluator(
0855: final PropertyProvider preprovider,
0856: final PropertyProvider[] providers) {
0857: this .preprovider = preprovider;
0858: this .providers = providers;
0859: // XXX defer until someone asks for them
0860: defs = ProjectManager.mutex().readAccess(
0861: new Mutex.Action<Map<String, String>>() {
0862: public Map<String, String> run() {
0863: return compose(preprovider, providers);
0864: }
0865: });
0866: // XXX defer until someone is listening?
0867: if (preprovider != null) {
0868: preprovider.addChangeListener(WeakListeners.change(
0869: this , preprovider));
0870: }
0871: for (PropertyProvider pp : providers) {
0872: pp.addChangeListener(WeakListeners.change(this , pp));
0873: }
0874: }
0875:
0876: public String getProperty(final String prop) {
0877: return ProjectManager.mutex().readAccess(
0878: new Mutex.Action<String>() {
0879: public String run() {
0880: if (defs == null) {
0881: return null;
0882: }
0883: return defs.get(prop);
0884: }
0885: });
0886: }
0887:
0888: public String evaluate(final String text) {
0889: if (text == null) {
0890: throw new NullPointerException(
0891: "Attempted to pass null to PropertyEvaluator.evaluate"); // NOI18N
0892: }
0893: return ProjectManager.mutex().readAccess(
0894: new Mutex.Action<String>() {
0895: public String run() {
0896: if (defs == null) {
0897: return null;
0898: }
0899: Union2<String, Set<String>> result = substitute(
0900: text, defs, Collections
0901: .<String> emptySet());
0902: assert result.hasFirst() : "Unexpected result "
0903: + result
0904: + " from "
0905: + text
0906: + " on "
0907: + defs;
0908: return result.first();
0909: }
0910: });
0911: }
0912:
0913: public Map<String, String> getProperties() {
0914: return ProjectManager.mutex().readAccess(
0915: new Mutex.Action<Map<String, String>>() {
0916: public Map<String, String> run() {
0917: return defs;
0918: }
0919: });
0920: }
0921:
0922: public void addPropertyChangeListener(
0923: PropertyChangeListener listener) {
0924: synchronized (listeners) {
0925: listeners.add(listener);
0926: }
0927: }
0928:
0929: public void removePropertyChangeListener(
0930: PropertyChangeListener listener) {
0931: synchronized (listeners) {
0932: listeners.remove(listener);
0933: }
0934: }
0935:
0936: public void stateChanged(ChangeEvent e) {
0937: assert ProjectManager.mutex().isReadAccess()
0938: || ProjectManager.mutex().isWriteAccess();
0939: Map<String, String> newdefs = compose(preprovider,
0940: providers);
0941: // compose() may return null upon circularity errors
0942: Map<String, String> _defs = defs != null ? defs
0943: : Collections.<String, String> emptyMap();
0944: Map<String, String> _newdefs = newdefs != null ? newdefs
0945: : Collections.<String, String> emptyMap();
0946: if (!_defs.equals(_newdefs)) {
0947: Set<String> props = new HashSet<String>(_defs.keySet());
0948: props.addAll(_newdefs.keySet());
0949: List<PropertyChangeEvent> events = new LinkedList<PropertyChangeEvent>();
0950: for (String prop : props) {
0951: assert prop != null;
0952: String oldval = _defs.get(prop);
0953: String newval = _newdefs.get(prop);
0954: if (newval != null) {
0955: if (newval.equals(oldval)) {
0956: continue;
0957: }
0958: } else {
0959: assert oldval != null : "should not have had "
0960: + prop;
0961: }
0962: events.add(new PropertyChangeEvent(this , prop,
0963: oldval, newval));
0964: }
0965: assert !events.isEmpty();
0966: defs = newdefs;
0967: PropertyChangeListener[] _listeners;
0968: synchronized (listeners) {
0969: _listeners = listeners
0970: .toArray(new PropertyChangeListener[listeners
0971: .size()]);
0972: }
0973: for (PropertyChangeListener l : _listeners) {
0974: for (PropertyChangeEvent ev : events) {
0975: l.propertyChange(ev);
0976: }
0977: }
0978: }
0979: }
0980:
0981: private static Map<String, String> compose(
0982: PropertyProvider preprovider,
0983: PropertyProvider[] providers) {
0984: assert ProjectManager.mutex().isReadAccess()
0985: || ProjectManager.mutex().isWriteAccess();
0986: Map<String, String> predefs;
0987: if (preprovider != null) {
0988: predefs = preprovider.getProperties();
0989: } else {
0990: predefs = Collections.emptyMap();
0991: }
0992: List<Map<String, String>> defs = new ArrayList<Map<String, String>>(
0993: providers.length);
0994: for (PropertyProvider pp : providers) {
0995: defs.add(pp.getProperties());
0996: }
0997: return evaluateAll(predefs, defs);
0998: }
0999:
1000: }
1001:
1002: }
|