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.io.ByteArrayOutputStream;
0045: import java.io.File;
0046: import java.io.IOException;
0047: import java.io.OutputStream;
0048: import java.util.ArrayList;
0049: import java.util.HashSet;
0050: import java.util.Iterator;
0051: import java.util.List;
0052: import java.util.Set;
0053: import javax.xml.parsers.DocumentBuilder;
0054: import javax.xml.parsers.DocumentBuilderFactory;
0055: import javax.xml.parsers.ParserConfigurationException;
0056: import org.netbeans.api.project.Project;
0057: import org.netbeans.api.project.ProjectManager;
0058: import org.netbeans.api.project.ant.AntArtifact;
0059: import org.netbeans.modules.project.ant.AntBasedProjectFactorySingleton;
0060: import org.netbeans.modules.project.ant.FileChangeSupport;
0061: import org.netbeans.modules.project.ant.FileChangeSupportEvent;
0062: import org.netbeans.modules.project.ant.FileChangeSupportListener;
0063: import org.netbeans.modules.project.ant.ProjectLibraryProvider;
0064: import org.netbeans.modules.project.ant.UserQuestionHandler;
0065: import org.netbeans.modules.project.ant.Util;
0066: import org.netbeans.spi.project.AuxiliaryConfiguration;
0067: import org.netbeans.spi.project.CacheDirectoryProvider;
0068: import org.netbeans.spi.project.ProjectState;
0069: import org.netbeans.spi.queries.FileBuiltQueryImplementation;
0070: import org.netbeans.spi.queries.SharabilityQueryImplementation;
0071: import org.openide.ErrorManager;
0072: import org.openide.filesystems.FileLock;
0073: import org.openide.filesystems.FileObject;
0074: import org.openide.filesystems.FileSystem;
0075: import org.openide.filesystems.FileUtil;
0076: import org.openide.util.Mutex;
0077: import org.openide.util.MutexException;
0078: import org.openide.util.RequestProcessor;
0079: import org.openide.util.UserQuestionException;
0080: import org.openide.xml.XMLUtil;
0081: import org.w3c.dom.Document;
0082: import org.w3c.dom.Element;
0083: import org.w3c.dom.Node;
0084: import org.w3c.dom.NodeList;
0085: import org.xml.sax.InputSource;
0086: import org.xml.sax.SAXException;
0087:
0088: /**
0089: * Support class for implementing Ant-based projects.
0090: * @author Jesse Glick
0091: */
0092: public final class AntProjectHelper {
0093:
0094: /**
0095: * Relative path from project directory to the customary shared properties file.
0096: */
0097: public static final String PROJECT_PROPERTIES_PATH = "nbproject/project.properties"; // NOI18N
0098:
0099: /**
0100: * Relative path from project directory to the customary private properties file.
0101: */
0102: public static final String PRIVATE_PROPERTIES_PATH = "nbproject/private/private.properties"; // NOI18N
0103:
0104: /**
0105: * Relative path from project directory to the required shared project metadata file.
0106: */
0107: public static final String PROJECT_XML_PATH = AntBasedProjectFactorySingleton.PROJECT_XML_PATH;
0108:
0109: /**
0110: * Relative path from project directory to the required private project metadata file.
0111: */
0112: public static final String PRIVATE_XML_PATH = "nbproject/private/private.xml"; // NOI18N
0113:
0114: /**
0115: * XML namespace of Ant projects.
0116: */
0117: static final String PROJECT_NS = AntBasedProjectFactorySingleton.PROJECT_NS;
0118:
0119: /**
0120: * XML namespace of private component of Ant projects.
0121: */
0122: static final String PRIVATE_NS = "http://www.netbeans.org/ns/project-private/1"; // NOI18N
0123:
0124: static {
0125: AntBasedProjectFactorySingleton.HELPER_CALLBACK = new AntBasedProjectFactorySingleton.AntProjectHelperCallback() {
0126: public AntProjectHelper createHelper(FileObject dir,
0127: Document projectXml, ProjectState state,
0128: AntBasedProjectType type) {
0129: return new AntProjectHelper(dir, projectXml, state,
0130: type);
0131: }
0132:
0133: public void save(AntProjectHelper helper)
0134: throws IOException {
0135: helper.save();
0136: }
0137: };
0138: }
0139:
0140: private static final RequestProcessor RP = new RequestProcessor(
0141: "AntProjectHelper.RP"); // NOI18N
0142:
0143: /**
0144: * Project base directory.
0145: */
0146: private final FileObject dir;
0147:
0148: /**
0149: * State object permitting modifications.
0150: */
0151: private final ProjectState state;
0152:
0153: /**
0154: * Ant-based project type factory.
0155: */
0156: private final AntBasedProjectType type;
0157:
0158: /**
0159: * Cached project.xml parse (null if not loaded).
0160: * Access within {@link #modifiedMetadataPaths} monitor.
0161: */
0162: private Document projectXml;
0163:
0164: /**
0165: * Cached private.xml parse (null if not loaded).
0166: * Access within {@link #modifiedMetadataPaths} monitor.
0167: */
0168: private Document privateXml;
0169:
0170: /**
0171: * Set of relative paths to metadata files which have been modified
0172: * and which need to be saved.
0173: * Also server as a monitor for {@link #projectXml} and {@link #privateXml} accesses;
0174: * Xerces' DOM is not thread-safe <em>even for reading<em> (#50198).
0175: */
0176: private final Set<String> modifiedMetadataPaths = new HashSet<String>();
0177:
0178: /**
0179: * Registered listeners.
0180: * Access must be directly synchronized.
0181: */
0182: private final List<AntProjectListener> listeners = new ArrayList<AntProjectListener>();
0183:
0184: /**
0185: * List of loaded properties.
0186: */
0187: private final ProjectProperties properties;
0188:
0189: /** Listener to XML files; needs to be held as an instance field so it is not GC'd */
0190: private final FileChangeSupportListener fileListener;
0191:
0192: /** True if currently saving XML files. */
0193: private boolean writingXML = false;
0194:
0195: /**
0196: * Hook waiting to be called. See issue #57794.
0197: */
0198: private ProjectXmlSavedHook pendingHook;
0199: /**
0200: * Number of metadata files remaining to be written before {@link #pendingHook} can be called.
0201: * Javadoc for {@link ProjectXmlSavedHook} only guarantees that project.xml will be written,
0202: * but best to be safe and make sure also private.xml and *.properties are too.
0203: */
0204: private int pendingHookCount;
0205:
0206: // XXX lock any loaded XML files while the project is modified, to prevent manual editing,
0207: // and reload any modified files if the project is unmodified
0208:
0209: private AntProjectHelper(FileObject dir, Document projectXml,
0210: ProjectState state, AntBasedProjectType type) {
0211: this .dir = dir;
0212: assert dir != null && FileUtil.toFile(dir) != null;
0213: this .state = state;
0214: assert state != null;
0215: this .type = type;
0216: assert type != null;
0217: this .projectXml = projectXml;
0218: assert projectXml != null;
0219: properties = new ProjectProperties(this );
0220: fileListener = new FileListener();
0221: FileChangeSupport.DEFAULT.addListener(fileListener,
0222: resolveFile(PROJECT_XML_PATH));
0223: FileChangeSupport.DEFAULT.addListener(fileListener,
0224: resolveFile(PRIVATE_XML_PATH));
0225: }
0226:
0227: /**
0228: * Get the corresponding Ant-based project type factory.
0229: */
0230: AntBasedProjectType getType() {
0231: return type;
0232: }
0233:
0234: /**
0235: * Retrieve project.xml or private.xml, loading from disk as needed.
0236: * private.xml is created as a skeleton on demand.
0237: */
0238: private Document getConfigurationXml(boolean shared) {
0239: assert ProjectManager.mutex().isReadAccess()
0240: || ProjectManager.mutex().isWriteAccess();
0241: assert Thread.holdsLock(modifiedMetadataPaths);
0242: Document xml = shared ? projectXml : privateXml;
0243: if (xml == null) {
0244: String path = shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH;
0245: xml = loadXml(path);
0246: if (xml == null) {
0247: // Missing or broken; create a skeleton.
0248: String element = shared ? "project" : "project-private"; // NOI18N
0249: String ns = shared ? PROJECT_NS : PRIVATE_NS;
0250: xml = XMLUtil.createDocument(element, ns, null, null);
0251: if (shared) {
0252: // #46048: need to generate minimal compliant XML skeleton.
0253: Element typeEl = xml.createElementNS(PROJECT_NS,
0254: "type"); // NOI18N
0255: typeEl.appendChild(xml.createTextNode(getType()
0256: .getType()));
0257: xml.getDocumentElement().appendChild(typeEl);
0258: xml.getDocumentElement().appendChild(
0259: xml.createElementNS(PROJECT_NS,
0260: "configuration")); // NOI18N
0261: }
0262: }
0263: if (shared) {
0264: projectXml = xml;
0265: } else {
0266: privateXml = xml;
0267: }
0268: }
0269: assert xml != null;
0270: return xml;
0271: }
0272:
0273: /**
0274: * If true, do not report XML load errors.
0275: * For use only by unit tests.
0276: */
0277: static boolean QUIETLY_SWALLOW_XML_LOAD_ERRORS = false;
0278:
0279: /**
0280: * Try to load a config XML file from a named path.
0281: * If the file does not exist, or there is any load error, return null.
0282: */
0283: private Document loadXml(String path) {
0284: assert ProjectManager.mutex().isReadAccess()
0285: || ProjectManager.mutex().isWriteAccess();
0286: assert Thread.holdsLock(modifiedMetadataPaths);
0287: FileObject xml = dir.getFileObject(path);
0288: if (xml == null || !xml.isData()) {
0289: return null;
0290: }
0291: File f = FileUtil.toFile(xml);
0292: assert f != null;
0293: try {
0294: return XMLUtil.parse(new InputSource(f.toURI().toString()),
0295: false, true, Util.defaultErrorHandler(), null);
0296: } catch (IOException e) {
0297: if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) {
0298: ErrorManager.getDefault().notify(
0299: ErrorManager.INFORMATIONAL, e);
0300: }
0301: } catch (SAXException e) {
0302: if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) {
0303: ErrorManager.getDefault().notify(
0304: ErrorManager.INFORMATIONAL, e);
0305: }
0306: }
0307: return null;
0308: }
0309:
0310: /**
0311: * Save an XML config file to a named path.
0312: * If the file does not yet exist, it is created.
0313: */
0314: private FileLock saveXml(final Document doc, final String path)
0315: throws IOException {
0316: assert ProjectManager.mutex().isWriteAccess();
0317: assert !writingXML;
0318: assert Thread.holdsLock(modifiedMetadataPaths);
0319: final FileLock[] _lock = new FileLock[1];
0320: writingXML = true;
0321: try {
0322: dir.getFileSystem().runAtomicAction(
0323: new FileSystem.AtomicAction() {
0324: public void run() throws IOException {
0325: // Keep a copy of xml *while holding modifiedMetadataPaths monitor*.
0326: ByteArrayOutputStream baos = new ByteArrayOutputStream();
0327: XMLUtil.write(doc, baos, "UTF-8"); // NOI18N
0328: final byte[] data = baos.toByteArray();
0329: final FileObject xml = FileUtil.createData(
0330: dir, path);
0331: try {
0332: _lock[0] = xml.lock(); // unlocked by {@link #save}
0333: OutputStream os = xml
0334: .getOutputStream(_lock[0]);
0335: try {
0336: os.write(data);
0337: } finally {
0338: os.close();
0339: }
0340: } catch (UserQuestionException uqe) { // #46089
0341: needPendingHook();
0342: UserQuestionHandler
0343: .handle(
0344: uqe,
0345: new UserQuestionHandler.Callback() {
0346: public void accepted() {
0347: // Try again.
0348: assert !writingXML;
0349: writingXML = true;
0350: try {
0351: FileLock lock = xml
0352: .lock();
0353: try {
0354: OutputStream os = xml
0355: .getOutputStream(lock);
0356: try {
0357: os
0358: .write(data);
0359: } finally {
0360: os
0361: .close();
0362: }
0363: } finally {
0364: lock
0365: .releaseLock();
0366: }
0367: maybeCallPendingHook();
0368: } catch (IOException e) {
0369: // Oh well.
0370: ErrorManager
0371: .getDefault()
0372: .notify(
0373: e);
0374: reload();
0375: } finally {
0376: writingXML = false;
0377: }
0378: }
0379:
0380: public void denied() {
0381: reload();
0382: }
0383:
0384: public void error(
0385: IOException e) {
0386: ErrorManager
0387: .getDefault()
0388: .notify(
0389: e);
0390: reload();
0391: }
0392:
0393: private void reload() {
0394: // Revert the save.
0395: if (path
0396: .equals(PROJECT_XML_PATH)) {
0397: synchronized (modifiedMetadataPaths) {
0398: projectXml = null;
0399: }
0400: } else {
0401: assert path
0402: .equals(PRIVATE_XML_PATH) : path;
0403: synchronized (modifiedMetadataPaths) {
0404: privateXml = null;
0405: }
0406: }
0407: fireExternalChange(path);
0408: cancelPendingHook();
0409: }
0410: });
0411: }
0412: }
0413: });
0414: } finally {
0415: writingXML = false;
0416: }
0417: return _lock[0];
0418: }
0419:
0420: /**
0421: * Get the <code><configuration></code> element of project.xml
0422: * or the document element of private.xml.
0423: * Beneath this point you can load and store configuration fragments.
0424: * @param shared if true, use project.xml, else private.xml
0425: * @return the data root
0426: */
0427: private Element getConfigurationDataRoot(boolean shared) {
0428: assert ProjectManager.mutex().isReadAccess()
0429: || ProjectManager.mutex().isWriteAccess();
0430: assert Thread.holdsLock(modifiedMetadataPaths);
0431: Document doc = getConfigurationXml(shared);
0432: if (shared) {
0433: Element project = doc.getDocumentElement();
0434: Element config = Util.findElement(project, "configuration",
0435: PROJECT_NS); // NOI18N
0436: assert config != null;
0437: return config;
0438: } else {
0439: return doc.getDocumentElement();
0440: }
0441: }
0442:
0443: /**
0444: * Add a listener to changes in the project configuration.
0445: * <p>Thread-safe.
0446: * @param listener a listener to add
0447: */
0448: public void addAntProjectListener(AntProjectListener listener) {
0449: synchronized (listeners) {
0450: listeners.add(listener);
0451: }
0452: }
0453:
0454: /**
0455: * Remove a listener to changes in the project configuration.
0456: * <p>Thread-safe.
0457: * @param listener a listener to remove
0458: */
0459: public void removeAntProjectListener(AntProjectListener listener) {
0460: synchronized (listeners) {
0461: listeners.remove(listener);
0462: }
0463: }
0464:
0465: /**
0466: * Fire a change of external provenance to all listeners.
0467: * Acquires write access.
0468: * @param path path to the changed file (XML or properties)
0469: */
0470: void fireExternalChange(final String path) {
0471: final Mutex.Action<Void> action = new Mutex.Action<Void>() {
0472: public Void run() {
0473: fireChange(path, false);
0474: return null;
0475: }
0476: };
0477: if (ProjectManager.mutex().isWriteAccess()
0478: || ProjectLibraryProvider.FIRE_CHANGES_SYNCH) {
0479: // Run it right now. postReadRequest would be too late.
0480: ProjectManager.mutex().readAccess(action);
0481: } else if (ProjectManager.mutex().isReadAccess()) {
0482: // Run immediately also. No need to switch to read access.
0483: action.run();
0484: } else {
0485: // Not safe to acquire a new lock, so run later in read access.
0486: RP.post(new Runnable() {
0487: public void run() {
0488: ProjectManager.mutex().readAccess(action);
0489: }
0490: });
0491: }
0492: }
0493:
0494: /**
0495: * Fire a change to all listeners.
0496: * Must be called from write access; enters read access while firing.
0497: * @param path path to the changed file (XML or properties)
0498: * @param expected true if the result of an API-initiated change, false if from external causes
0499: */
0500: private void fireChange(String path, boolean expected) {
0501: assert ProjectManager.mutex().isReadAccess()
0502: || ProjectManager.mutex().isWriteAccess();
0503: final AntProjectListener[] _listeners;
0504: synchronized (listeners) {
0505: if (listeners.isEmpty()) {
0506: return;
0507: }
0508: _listeners = listeners
0509: .toArray(new AntProjectListener[listeners.size()]);
0510: }
0511: final AntProjectEvent ev = new AntProjectEvent(this , path,
0512: expected);
0513: final boolean xml = path.equals(PROJECT_XML_PATH)
0514: || path.equals(PRIVATE_XML_PATH);
0515: ProjectManager.mutex().readAccess(new Mutex.Action<Void>() {
0516: public Void run() {
0517: for (AntProjectListener l : _listeners) {
0518: try {
0519: if (xml) {
0520: l.configurationXmlChanged(ev);
0521: } else {
0522: l.propertiesChanged(ev);
0523: }
0524: } catch (RuntimeException e) {
0525: // Don't prevent other listeners from being notified.
0526: ErrorManager.getDefault().notify(e);
0527: }
0528: }
0529: return null;
0530: }
0531: });
0532: }
0533:
0534: /**
0535: * Call when explicitly modifying some piece of metadata.
0536: */
0537: private void modifying(String path) {
0538: assert ProjectManager.mutex().isWriteAccess();
0539: state.markModified();
0540: synchronized (modifiedMetadataPaths) {
0541: modifiedMetadataPaths.add(path);
0542: }
0543: fireChange(path, true);
0544: }
0545:
0546: /**
0547: * Get the top-level project directory.
0548: * @return the project directory beneath which everything in the project lies
0549: */
0550: public FileObject getProjectDirectory() {
0551: return dir;
0552: }
0553:
0554: /**Notification that this project has been deleted.
0555: * @see org.netbeans.spi.project.ProjectState#notifyDeleted
0556: *
0557: * @since 1.8
0558: */
0559: public void notifyDeleted() {
0560: state.notifyDeleted();
0561: }
0562:
0563: /**
0564: * Mark this project as being modified without actually changing anything in it.
0565: * Should only be called from {@link ProjectGenerator#createProject}.
0566: */
0567: void markModified() {
0568: assert ProjectManager.mutex().isWriteAccess();
0569: state.markModified();
0570: // To make sure projectXmlSaved is called:
0571: synchronized (modifiedMetadataPaths) {
0572: modifiedMetadataPaths.add(PROJECT_XML_PATH);
0573: }
0574: }
0575:
0576: /**
0577: * Check whether this project is currently modified including modifications
0578: * to <code>project.xml</code>.
0579: * Access from GeneratedFilesHelper.
0580: */
0581: boolean isProjectXmlModified() {
0582: assert ProjectManager.mutex().isReadAccess()
0583: || ProjectManager.mutex().isWriteAccess();
0584: return modifiedMetadataPaths.contains(PROJECT_XML_PATH);
0585: }
0586:
0587: /**
0588: * Save all cached project metadata.
0589: * If <code>project.xml</code> was one of the modified files, then
0590: * {@link AntBasedProjectType#projectXmlSaved} is called, presumably
0591: * creating <code>build-impl.xml</code> and/or <code>build.xml</code>.
0592: */
0593: private void save() throws IOException {
0594: assert ProjectManager.mutex().isWriteAccess();
0595: if (!getProjectDirectory().isValid()) {
0596: //ProjectManager.saveProject() is called when project is deleted externally..
0597: return;
0598: }
0599: Set<FileLock> locks = new HashSet<FileLock>();
0600: try {
0601: synchronized (modifiedMetadataPaths) {
0602: assert !modifiedMetadataPaths.isEmpty();
0603: assert pendingHook == null;
0604: if (modifiedMetadataPaths.contains(PROJECT_XML_PATH)) {
0605: // Saving project.xml so look for that hook.
0606: Project p = AntBasedProjectFactorySingleton
0607: .getProjectFor(this );
0608: pendingHook = p.getLookup().lookup(
0609: ProjectXmlSavedHook.class);
0610: // might still be null
0611: }
0612: Iterator<String> it = modifiedMetadataPaths.iterator();
0613: while (it.hasNext()) {
0614: String path = it.next();
0615: if (path.equals(PROJECT_XML_PATH)) {
0616: assert projectXml != null;
0617: locks.add(saveXml(projectXml, path));
0618: } else if (path.equals(PRIVATE_XML_PATH)) {
0619: assert privateXml != null;
0620: locks.add(saveXml(privateXml, path));
0621: } else {
0622: // All else is assumed to be a properties file.
0623: locks.add(properties.write(path));
0624: }
0625: // As metadata files are saved, take them off the modified list.
0626: it.remove();
0627: }
0628: if (pendingHook != null && pendingHookCount == 0) {
0629: try {
0630: pendingHook.projectXmlSaved();
0631: } catch (IOException e) {
0632: // Treat it as still modified.
0633: modifiedMetadataPaths.add(PROJECT_XML_PATH);
0634: throw e;
0635: }
0636: }
0637: }
0638: } finally {
0639: // #57791: release locks outside synchronized block.
0640: locks.remove(null);
0641: for (FileLock lock : locks) {
0642: lock.releaseLock();
0643: }
0644: // More #57794.
0645: if (pendingHookCount == 0) {
0646: pendingHook = null;
0647: }
0648: }
0649: }
0650:
0651: /** See issue #57794. */
0652: void maybeCallPendingHook() {
0653: // XXX synchronization of this method?
0654: assert pendingHookCount > 0;
0655: pendingHookCount--;
0656: //#67465: the pendingHook may be null if project.xml is not being written
0657: //eg. only project.properties is being saved:
0658: if (pendingHookCount == 0 && pendingHook != null) {
0659: try {
0660: ProjectManager.mutex().writeAccess(
0661: new Mutex.ExceptionAction<Void>() {
0662: public Void run() throws IOException {
0663: pendingHook.projectXmlSaved();
0664: return null;
0665: }
0666: });
0667: } catch (MutexException e) {
0668: // XXX mark project modified again??
0669: ErrorManager.getDefault().notify(e);
0670: } finally {
0671: pendingHook = null;
0672: }
0673: }
0674: }
0675:
0676: void cancelPendingHook() {
0677: assert pendingHookCount > 0;
0678: pendingHookCount--;
0679: if (pendingHookCount == 0) {
0680: pendingHook = null;
0681: }
0682: }
0683:
0684: void needPendingHook() {
0685: pendingHookCount++;
0686: }
0687:
0688: /**
0689: * Load a property file from some location in the project.
0690: * The returned object may be edited but you must call {@link #putProperties}
0691: * to save any changes you make.
0692: * If the file does not (yet) exist or could not be loaded for whatever reason,
0693: * an empty properties list is returned instead.
0694: * @param path a relative URI in the project directory, e.g.
0695: * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
0696: * @return a set of properties
0697: */
0698: public EditableProperties getProperties(final String path) {
0699: if (path.equals(AntProjectHelper.PROJECT_XML_PATH)
0700: || path.equals(AntProjectHelper.PRIVATE_XML_PATH)) {
0701: throw new IllegalArgumentException(
0702: "Attempt to load properties from a project XML file"); // NOI18N
0703: }
0704: return ProjectManager.mutex().readAccess(
0705: new Mutex.Action<EditableProperties>() {
0706: public EditableProperties run() {
0707: return properties.getProperties(path);
0708: }
0709: });
0710: }
0711:
0712: /**
0713: * Store a property file to some location in the project.
0714: * A clone will be made of the supplied properties file so as to snapshot it.
0715: * The new properties are not actually stored to disk immediately, but the project
0716: * is marked modified so that they will be later.
0717: * You can store to a path that does not yet exist and the file will be created
0718: * if and when the project is saved.
0719: * If the old value is the same as the new, nothing is done.
0720: * Otherwise an expected properties change event is fired.
0721: * <p>Acquires write access from {@link ProjectManager#mutex}. However, you are well
0722: * advised to explicitly enclose a <em>complete</em> operation within write access,
0723: * starting with {@link #getProperties}, to prevent race conditions.
0724: * @param path a relative URI in the project directory, e.g.
0725: * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
0726: * @param props a set of properties to store, or null to delete any existing properties file there
0727: */
0728: public void putProperties(final String path,
0729: final EditableProperties props) {
0730: if (path.equals(AntProjectHelper.PROJECT_XML_PATH)
0731: || path.equals(AntProjectHelper.PRIVATE_XML_PATH)) {
0732: throw new IllegalArgumentException(
0733: "Attempt to store properties from a project XML file"); // NOI18N
0734: }
0735: ProjectManager.mutex().writeAccess(new Mutex.Action<Void>() {
0736: public Void run() {
0737: if (properties.putProperties(path, props)) {
0738: modifying(path);
0739: }
0740: return null;
0741: }
0742: });
0743: }
0744:
0745: /**
0746: * Get a property provider that works with loadable project properties.
0747: * Its current values should match {@link #getProperties}, and calls to
0748: * {@link #putProperties} should cause it to fire changes.
0749: * @param path a relative URI in the project directory, e.g.
0750: * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
0751: * @return a property provider implementation
0752: */
0753: public PropertyProvider getPropertyProvider(final String path) {
0754: if (path.equals(AntProjectHelper.PROJECT_XML_PATH)
0755: || path.equals(AntProjectHelper.PRIVATE_XML_PATH)) {
0756: throw new IllegalArgumentException(
0757: "Attempt to store properties from a project XML file"); // NOI18N
0758: }
0759: return ProjectManager.mutex().readAccess(
0760: new Mutex.Action<PropertyProvider>() {
0761: public PropertyProvider run() {
0762: return properties.getPropertyProvider(path);
0763: }
0764: });
0765: }
0766:
0767: /**
0768: * Get the primary configuration data for this project.
0769: * The returned element will be named according to
0770: * {@link AntBasedProjectType#getPrimaryConfigurationDataElementName} and
0771: * {@link AntBasedProjectType#getPrimaryConfigurationDataElementNamespace}.
0772: * The project may read this document fragment to get custom information
0773: * from <code>nbproject/project.xml</code> and <code>nbproject/private/private.xml</code>.
0774: * The fragment will have no parent node and while it may be modified, you must
0775: * use {@link #putPrimaryConfigurationData} to store any changes.
0776: * @param shared if true, refers to <code>project.xml</code>, else refers to
0777: * <code>private.xml</code>
0778: * @return the configuration data that is available
0779: */
0780: public Element getPrimaryConfigurationData(final boolean shared) {
0781: final String name = type
0782: .getPrimaryConfigurationDataElementName(shared);
0783: assert name.indexOf(':') == -1;
0784: final String namespace = type
0785: .getPrimaryConfigurationDataElementNamespace(shared);
0786: assert namespace != null && namespace.length() > 0;
0787: return ProjectManager.mutex().readAccess(
0788: new Mutex.Action<Element>() {
0789: public Element run() {
0790: synchronized (modifiedMetadataPaths) {
0791: Element el = getConfigurationFragment(name,
0792: namespace, shared);
0793: if (el != null) {
0794: return el;
0795: } else {
0796: // No such data, corrupt file.
0797: return cloneSafely(getConfigurationXml(
0798: shared).createElementNS(
0799: namespace, name));
0800: }
0801: }
0802: }
0803: });
0804: }
0805:
0806: /**
0807: * Store the primary configuration data for this project.
0808: * The supplied element must be named according to
0809: * {@link AntBasedProjectType#getPrimaryConfigurationDataElementName} and
0810: * {@link AntBasedProjectType#getPrimaryConfigurationDataElementNamespace}.
0811: * The project may save this document fragment to set custom information
0812: * in <code>nbproject/project.xml</code> and <code>nbproject/private/private.xml</code>.
0813: * The fragment will be cloned and so further modifications will have no effect.
0814: * <p>Acquires write access from {@link ProjectManager#mutex}. However, you are well
0815: * advised to explicitly enclose a <em>complete</em> operation within write access,
0816: * starting with {@link #getPrimaryConfigurationData}, to prevent race conditions.
0817: * @param data the desired new configuration data
0818: * @param shared if true, refers to <code>project.xml</code>, else refers to
0819: * <code>private.xml</code>
0820: * @throws IllegalArgumentException if the element is not correctly named
0821: */
0822: public void putPrimaryConfigurationData(Element data, boolean shared)
0823: throws IllegalArgumentException {
0824: String name = type
0825: .getPrimaryConfigurationDataElementName(shared);
0826: assert name.indexOf(':') == -1;
0827: String namespace = type
0828: .getPrimaryConfigurationDataElementNamespace(shared);
0829: assert namespace != null && namespace.length() > 0;
0830: if (!name.equals(data.getLocalName())
0831: || !namespace.equals(data.getNamespaceURI())) {
0832: throw new IllegalArgumentException(
0833: "Wrong name/namespace: expected {" + namespace
0834: + "}" + name + " but was {"
0835: + data.getNamespaceURI() + "}"
0836: + data.getLocalName()); // NOI18N
0837: }
0838: putConfigurationFragment(data, shared);
0839: }
0840:
0841: private final class FileListener implements
0842: FileChangeSupportListener {
0843:
0844: public FileListener() {
0845: }
0846:
0847: private void change(File f) {
0848: if (writingXML) {
0849: return;
0850: }
0851: String path;
0852: synchronized (modifiedMetadataPaths) {
0853: if (f.equals(resolveFile(PROJECT_XML_PATH))) {
0854: if (modifiedMetadataPaths
0855: .contains(PROJECT_XML_PATH)) {
0856: //#68872: don't do anything if the given file has non-saved changes:
0857: return;
0858: }
0859: path = PROJECT_XML_PATH;
0860: projectXml = null;
0861: } else if (f.equals(resolveFile(PRIVATE_XML_PATH))) {
0862: if (modifiedMetadataPaths
0863: .contains(PRIVATE_XML_PATH)) {
0864: //#68872: don't do anything if the given file has non-saved changes:
0865: return;
0866: }
0867: path = PRIVATE_XML_PATH;
0868: privateXml = null;
0869: } else {
0870: throw new AssertionError(
0871: "Unexpected file change in " + f); // NOI18N
0872: }
0873: }
0874: fireExternalChange(path);
0875: }
0876:
0877: public void fileCreated(FileChangeSupportEvent event) {
0878: change(event.getPath());
0879: }
0880:
0881: public void fileDeleted(FileChangeSupportEvent event) {
0882: change(event.getPath());
0883: }
0884:
0885: public void fileModified(FileChangeSupportEvent event) {
0886: change(event.getPath());
0887: }
0888:
0889: }
0890:
0891: /**
0892: * Get a piece of the configuration subtree by name.
0893: * @param elementName the simple XML element name expected
0894: * @param namespace the XML namespace expected
0895: * @param shared to use project.xml vs. private.xml
0896: * @return (a clone of) the named configuration fragment, or null if it does not exist
0897: */
0898: Element getConfigurationFragment(final String elementName,
0899: final String namespace, final boolean shared) {
0900: return ProjectManager.mutex().readAccess(
0901: new Mutex.Action<Element>() {
0902: public Element run() {
0903: synchronized (modifiedMetadataPaths) {
0904: Element root = getConfigurationDataRoot(shared);
0905: Element data = Util.findElement(root,
0906: elementName, namespace);
0907: if (data != null) {
0908: return cloneSafely(data);
0909: } else {
0910: return null;
0911: }
0912: }
0913: }
0914: });
0915: }
0916:
0917: private static final DocumentBuilder db;
0918: static {
0919: try {
0920: db = DocumentBuilderFactory.newInstance()
0921: .newDocumentBuilder();
0922: } catch (ParserConfigurationException e) {
0923: throw new AssertionError(e);
0924: }
0925: }
0926:
0927: private static Element cloneSafely(Element el) {
0928: // #50198: for thread safety, use a separate document.
0929: // Using XMLUtil.createDocument is much too slow.
0930: synchronized (db) {
0931: Document dummy = db.newDocument();
0932: return (Element) dummy.importNode(el, true);
0933: }
0934: }
0935:
0936: /**
0937: * Store a piece of the configuration subtree by name.
0938: * @param fragment a piece of the subtree to store (overwrite or add)
0939: * @param shared to use project.xml vs. private.xml
0940: */
0941: void putConfigurationFragment(final Element fragment,
0942: final boolean shared) {
0943: ProjectManager.mutex().writeAccess(new Mutex.Action<Void>() {
0944: public Void run() {
0945: synchronized (modifiedMetadataPaths) {
0946: Element root = getConfigurationDataRoot(shared);
0947: Element existing = Util
0948: .findElement(root, fragment.getLocalName(),
0949: fragment.getNamespaceURI());
0950: // XXX first compare to existing and return if the same
0951: if (existing != null) {
0952: root.removeChild(existing);
0953: }
0954: // the children are alphabetize: find correct place to insert new node
0955: Node ref = null;
0956: NodeList list = root.getChildNodes();
0957: for (int i = 0; i < list.getLength(); i++) {
0958: Node node = list.item(i);
0959: if (node.getNodeType() != Node.ELEMENT_NODE) {
0960: continue;
0961: }
0962: int comparison = node.getNodeName().compareTo(
0963: fragment.getNodeName());
0964: if (comparison == 0) {
0965: comparison = node.getNamespaceURI()
0966: .compareTo(
0967: fragment.getNamespaceURI());
0968: }
0969: if (comparison > 0) {
0970: ref = node;
0971: break;
0972: }
0973: }
0974: root.insertBefore(root.getOwnerDocument()
0975: .importNode(fragment, true), ref);
0976: modifying(shared ? PROJECT_XML_PATH
0977: : PRIVATE_XML_PATH);
0978: }
0979: return null;
0980: }
0981: });
0982: }
0983:
0984: /**
0985: * Remove a piece of the configuration subtree by name.
0986: * @param elementName the simple XML element name expected
0987: * @param namespace the XML namespace expected
0988: * @param shared to use project.xml vs. private.xml
0989: * @return true if anything was actually removed
0990: */
0991: boolean removeConfigurationFragment(final String elementName,
0992: final String namespace, final boolean shared) {
0993: return ProjectManager.mutex().writeAccess(
0994: new Mutex.Action<Boolean>() {
0995: public Boolean run() {
0996: synchronized (modifiedMetadataPaths) {
0997: Element root = getConfigurationDataRoot(shared);
0998: Element data = Util.findElement(root,
0999: elementName, namespace);
1000: if (data != null) {
1001: root.removeChild(data);
1002: modifying(shared ? PROJECT_XML_PATH
1003: : PRIVATE_XML_PATH);
1004: return true;
1005: } else {
1006: return false;
1007: }
1008: }
1009: }
1010: });
1011: }
1012:
1013: /**
1014: * Create an object permitting this project to store auxiliary configuration.
1015: * Would be placed into the project's lookup.
1016: * @return an auxiliary configuration provider object suitable for the project lookup
1017: */
1018: public AuxiliaryConfiguration createAuxiliaryConfiguration() {
1019: return new ExtensibleMetadataProviderImpl(this );
1020: }
1021:
1022: /**
1023: * Create an object permitting this project to expose a cache directory.
1024: * Would be placed into the project's lookup.
1025: * @return a cache directory provider object suitable for the project lookup
1026: */
1027: public CacheDirectoryProvider createCacheDirectoryProvider() {
1028: return new ExtensibleMetadataProviderImpl(this );
1029: }
1030:
1031: /**
1032: * Create an implementation of {@link org.netbeans.api.queries.FileBuiltQuery} that works with files
1033: * within the project based on simple glob pattern mappings.
1034: * <p>
1035: * It is intended to be
1036: * placed in {@link org.netbeans.api.project.Project#getLookup}.
1037: * <p>
1038: * It will return status objects for any files in the project matching a source
1039: * glob pattern - this must include exactly one asterisk (<code>*</code>)
1040: * representing a variable portion of a source file path (always slash-separated
1041: * and relative to the project directory) and may include some Ant property
1042: * references which will be resolved as per the property evaluator.
1043: * A file is considered out of date if there is no file represented by the
1044: * matching target pattern (which has the same format), or the target file is older
1045: * than the source file, or the source file is modified as per
1046: * {@link org.openide.loaders.DataObject#isModified}.
1047: * An attempt is made to fire changes from the status object whenever the result
1048: * should change from one call to the next.
1049: * <p>
1050: * The (evaluated) source and target patterns may be relative, resolved against
1051: * the project directory (perhaps going outside it), or absolute.
1052: * </p>
1053: * <div class="nonnormative">
1054: * <p>
1055: * A typical set of source and target patterns would be:
1056: * </p>
1057: * <ol>
1058: * <li><samp>${src.dir}/*.java</samp>
1059: * <li><samp>${test.src.dir}/*.java</samp>
1060: * </ol>
1061: * <ol>
1062: * <li><samp>${build.classes.dir}/*.class</samp>
1063: * <li><samp>${test.build.classes.dir}/*.class</samp>
1064: * </ol>
1065: * </div>
1066: * @param eval a property evaluator to interpret the patterns with
1067: * @param from a list of glob patterns for source files
1068: * @param to a matching list of glob patterns for built files
1069: * @return a query implementation
1070: * @throws IllegalArgumentException if either from or to patterns
1071: * have zero or multiple asterisks,
1072: * or the arrays are not of equal lengths
1073: */
1074: public FileBuiltQueryImplementation createGlobFileBuiltQuery(
1075: PropertyEvaluator eval, String[] from, String[] to)
1076: throws IllegalArgumentException {
1077: return new GlobFileBuiltQuery(this , eval, from, to);
1078: }
1079:
1080: /**
1081: * Create a basic implementation of {@link AntArtifact} which assumes everything of interest
1082: * is in a fixed location under a standard Ant-based project.
1083: * @param type the type of artifact, e.g. <a href="@JAVA/PROJECT@/org/netbeans/api/java/project/JavaProjectConstants.html#ARTIFACT_TYPE_JAR"><code>JavaProjectConstants.ARTIFACT_TYPE_JAR</code></a>
1084: * @param locationProperty an Ant property name giving the project-relative
1085: * location of the artifact, e.g. <samp>dist.jar</samp>
1086: * @param eval a way to evaluate the location property (e.g. {@link #getStandardPropertyEvaluator})
1087: * @param targetName the name of an Ant target which will build the artifact,
1088: * e.g. <samp>jar</samp>
1089: * @param cleanTargetName the name of an Ant target which will delete the artifact
1090: * (and maybe other build products), e.g. <samp>clean</samp>
1091: * @return an artifact
1092: */
1093: public AntArtifact createSimpleAntArtifact(String type,
1094: String locationProperty, PropertyEvaluator eval,
1095: String targetName, String cleanTargetName) {
1096: return new SimpleAntArtifact(this , type, locationProperty,
1097: eval, targetName, cleanTargetName);
1098: }
1099:
1100: /**
1101: * Create an implementation of the file sharability query.
1102: * You may specify a list of source roots to include that should be considered sharable,
1103: * as well as a list of build directories that should not be considered sharable.
1104: * <p>
1105: * The project directory itself is automatically included in the list of sharable directories
1106: * so you need not explicitly specify it.
1107: * Similarly, the <code>nbproject/private</code> subdirectory is automatically excluded
1108: * from VCS, so you do not need to explicitly specify it.
1109: * </p>
1110: * <p>
1111: * Any file (or directory) mentioned (explicitly or implicity) in the source
1112: * directory list but not in any of the build directory lists, and not containing
1113: * any build directories inside it, will be given as sharable. If a directory itself
1114: * is sharable but some directory inside it is not, it will be given as mixed.
1115: * A file or directory inside some build directory will be listed as not sharable.
1116: * A file or directory matching neither the source list nor the build directory list
1117: * will be treated as of unknown status, but in practice such a file should never
1118: * have been passed to this implementation anyway - {@link org.netbeans.api.queries.SharabilityQuery} will
1119: * normally only call an implementation in project lookup if the file is owned by
1120: * that project.
1121: * </p>
1122: * <p>
1123: * Each entry in either list should be a string evaluated first for Ant property
1124: * escapes (if any), then treated as a file path relative to the project directory
1125: * (or it may be absolute).
1126: * </p>
1127: * <p>
1128: * It is permitted, and harmless, to include items that overlap others. For example,
1129: * you can have both a directory and one of its children in the include list.
1130: * </p>
1131: * <p>
1132: * Whether or not you use this method, all files named <code>*-private.properties</code>
1133: * outside the project are marked unsharable, as are such files inside the project if currently referenced
1134: * as project libraries. (See {@link #getProjectLibrariesPropertyProvider}.)
1135: * </p>
1136: * <div class="nonnormative">
1137: * <p>
1138: * Typical usage would be:
1139: * </p>
1140: * <pre>
1141: * helper.createSharabilityQuery(helper.getStandardPropertyEvaluator(),
1142: * new String[] {"${src.dir}", "${test.src.dir}"},
1143: * new String[] {"${build.dir}", "${dist.dir}"})
1144: * </pre>
1145: * <p>
1146: * A quick rule of thumb is that the include list should contain any
1147: * source directories which <em>might</em> reside outside the project directory;
1148: * and the exclude list should contain any directories which you would want
1149: * to add to a <samp>.cvsignore</samp> file if using CVS (for example).
1150: * </p>
1151: * <p>
1152: * Note that in this case <samp>${src.dir}</samp> and <samp>${test.src.dir}</samp>
1153: * may be relative paths inside the project directory; relative paths pointing
1154: * outside of the project directory; or absolute paths (generally outside of the
1155: * project directory). If they refer to locations inside the project directory,
1156: * including them does nothing but is harmless - since the project directory itself
1157: * is always treated as sharable. If they refer to external locations, you will
1158: * need to also make sure that {@link org.netbeans.api.project.FileOwnerQuery} actually maps files in those
1159: * directories to this project, or else {@link org.netbeans.api.queries.SharabilityQuery} will never find
1160: * this implementation in your project lookup and may return <code>UNKNOWN</code>.
1161: * </p>
1162: * </div>
1163: * @param eval a property evaluator to interpret paths with
1164: * @param sourceRoots a list of additional paths to treat as sharable
1165: * @param buildDirectories a list of paths to treat as not sharable
1166: * @return a sharability query implementation suitable for the project lookup
1167: * @see Project#getLookup
1168: */
1169: public SharabilityQueryImplementation createSharabilityQuery(
1170: PropertyEvaluator eval, String[] sourceRoots,
1171: String[] buildDirectories) {
1172: String[] includes = new String[sourceRoots.length + 1];
1173: System.arraycopy(sourceRoots, 0, includes, 0,
1174: sourceRoots.length);
1175: includes[sourceRoots.length] = ""; // NOI18N
1176: String[] excludes = new String[buildDirectories.length + 1];
1177: System.arraycopy(buildDirectories, 0, excludes, 0,
1178: buildDirectories.length);
1179: excludes[buildDirectories.length] = "nbproject/private"; // NOI18N
1180: return new SharabilityQueryImpl(this , eval, includes, excludes);
1181: }
1182:
1183: /**
1184: * Get a property provider which defines <code>basedir</code> according to
1185: * the project directory and also copies all system properties in the current VM.
1186: * It may also define <code>ant.home</code> and <code>ant.core.lib</code> if it is able.
1187: * @return a stock property provider for initial Ant-related definitions
1188: * @see PropertyUtils#sequentialPropertyEvaluator
1189: */
1190: public PropertyProvider getStockPropertyPreprovider() {
1191: return properties.getStockPropertyPreprovider();
1192: }
1193:
1194: /**
1195: * Creates a property provider which can load definitions of project libraries.
1196: * If this project refers to any project library definition files, they will
1197: * be included, with <code>${base}</code> replaced by the appropriate value.
1198: * @return a property provider
1199: * @since org.netbeans.modules.project.ant/1 1.19
1200: * @see <a href="http://www.netbeans.org/ns/ant-project-libraries/1.xsd">Schema for project library references</a>
1201: */
1202: public PropertyProvider getProjectLibrariesPropertyProvider() {
1203: return ProjectLibraryProvider.createPropertyProvider(this );
1204: }
1205:
1206: /**
1207: * Is this project shared with other or not, that is is it using shrared
1208: * libraries or not.
1209: * @return <code>true</code> for shared project
1210: * @since org.netbeans.modules.project.ant/1 1.19
1211: */
1212: public boolean isSharableProject() {
1213: return getLibrariesLocation() != null;
1214: }
1215:
1216: /**
1217: * Returns location of shared libraries associated with this project or null.
1218: * @return relative or absolute OS path or null
1219: * @since org.netbeans.modules.project.ant/1 1.19
1220: */
1221: public String getLibrariesLocation() {
1222: return ProjectLibraryProvider.getLibrariesLocationText(this
1223: .createAuxiliaryConfiguration());
1224: }
1225:
1226: /**
1227: * Change project's associated shared libraries location. If location is
1228: * <code>null</code> then project will not have shared libraries and will
1229: * be considered as not being shared.
1230: *
1231: * @param location project relative or absolute OS path or null
1232: * @since org.netbeans.modules.project.ant/1 1.18
1233: */
1234: public void setLibrariesLocation(String location) {
1235: ProjectLibraryProvider.setLibrariesLocation(this , location);
1236: }
1237:
1238: /**
1239: * Get a property evaluator that can evaluate properties according to the default
1240: * file layout for Ant-based projects.
1241: * First, {@link #getStockPropertyPreprovider stock properties} are predefined.
1242: * Then {@link #PRIVATE_PROPERTIES_PATH} is loaded via {@link #getPropertyProvider},
1243: * then {@link #getProjectLibrariesPropertyProvider},
1244: * then global definitions from {@link PropertyUtils#globalPropertyProvider}
1245: * (though these may be overridden using the property <code>user.properties.file</code>
1246: * in <code>private.properties</code>), then {@link #PROJECT_PROPERTIES_PATH}.
1247: * @return a standard property evaluator
1248: */
1249: public PropertyEvaluator getStandardPropertyEvaluator() {
1250: return properties.getStandardPropertyEvaluator();
1251: }
1252:
1253: /**
1254: * Find an absolute file path from a possibly project-relative path.
1255: * @param filename a pathname which may be project-relative or absolute and may
1256: * use / or \ as the path separator
1257: * @return an absolute file corresponding to it
1258: */
1259: public File resolveFile(String filename) {
1260: if (filename == null) {
1261: throw new NullPointerException(
1262: "Attempted to pass a null filename to resolveFile"); // NOI18N
1263: }
1264: return PropertyUtils
1265: .resolveFile(FileUtil.toFile(dir), filename);
1266: }
1267:
1268: /**
1269: * Same as {@link #resolveFile}, but produce a <code>FileObject</code> if possible.
1270: * @param filename a pathname according to Ant conventions
1271: * @return a file object it represents, or null if there is no such file object in known filesystems
1272: */
1273: public FileObject resolveFileObject(String filename) {
1274: if (filename == null) {
1275: throw new NullPointerException(
1276: "Must pass a non-null filename"); // NOI18N
1277: }
1278: return PropertyUtils.resolveFileObject(dir, filename);
1279: }
1280:
1281: /**
1282: * Take an Ant-style path specification and convert it to a platform-specific absolute path.
1283: * The path separator characters are converted to the local convention, and individual
1284: * path components are resolved and cleaned up as for {@link #resolveFile}.
1285: * @param path an Ant-style abstract path
1286: * @return an absolute, locally usable path
1287: */
1288: public String resolvePath(String path) {
1289: if (path == null) {
1290: throw new NullPointerException("Must pass a non-null path"); // NOI18N
1291: }
1292: // XXX consider memoizing results since this is probably called a lot
1293: return PropertyUtils.resolvePath(FileUtil.toFile(dir), path);
1294: }
1295:
1296: @Override
1297: public String toString() {
1298: return "AntProjectHelper[" + getProjectDirectory() + "]"; // NOI18N
1299: }
1300:
1301: }
|