KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > netbeans > spi > project > support > ant > AntProjectHelper


1 /*
2  * The contents of this file are subject to the terms of the Common Development
3  * and Distribution License (the License). You may not use this file except in
4  * compliance with the License.
5  *
6  * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
7  * or http://www.netbeans.org/cddl.txt.
8  *
9  * When distributing Covered Code, include this CDDL Header Notice in each file
10  * and include the License file at http://www.netbeans.org/cddl.txt.
11  * If applicable, add the following below the CDDL Header, with the fields
12  * enclosed by brackets [] replaced by your own identifying information:
13  * "Portions Copyrighted [year] [name of copyright owner]"
14  *
15  * The Original Software is NetBeans. The Initial Developer of the Original
16  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
17  * Microsystems, Inc. All Rights Reserved.
18  */

19
20 package org.netbeans.spi.project.support.ant;
21
22 import java.io.ByteArrayOutputStream JavaDoc;
23 import java.io.File JavaDoc;
24 import java.io.IOException JavaDoc;
25 import java.io.OutputStream JavaDoc;
26 import java.util.ArrayList JavaDoc;
27 import java.util.HashSet JavaDoc;
28 import java.util.Iterator JavaDoc;
29 import java.util.List JavaDoc;
30 import java.util.Set JavaDoc;
31 import javax.xml.parsers.DocumentBuilder JavaDoc;
32 import javax.xml.parsers.DocumentBuilderFactory JavaDoc;
33 import javax.xml.parsers.ParserConfigurationException JavaDoc;
34 import org.netbeans.api.project.Project;
35 import org.netbeans.api.project.ProjectManager;
36 import org.netbeans.api.project.ant.AntArtifact;
37 import org.netbeans.modules.project.ant.AntBasedProjectFactorySingleton;
38 import org.netbeans.modules.project.ant.FileChangeSupport;
39 import org.netbeans.modules.project.ant.FileChangeSupportEvent;
40 import org.netbeans.modules.project.ant.FileChangeSupportListener;
41 import org.netbeans.modules.project.ant.UserQuestionHandler;
42 import org.netbeans.modules.project.ant.Util;
43 import org.netbeans.spi.project.AuxiliaryConfiguration;
44 import org.netbeans.spi.project.CacheDirectoryProvider;
45 import org.netbeans.spi.project.ProjectState;
46 import org.netbeans.spi.queries.FileBuiltQueryImplementation;
47 import org.netbeans.spi.queries.SharabilityQueryImplementation;
48 import org.openide.ErrorManager;
49 import org.openide.filesystems.FileLock;
50 import org.openide.filesystems.FileObject;
51 import org.openide.filesystems.FileSystem;
52 import org.openide.filesystems.FileUtil;
53 import org.openide.util.Mutex;
54 import org.openide.util.MutexException;
55 import org.openide.util.RequestProcessor;
56 import org.openide.util.UserQuestionException;
57 import org.openide.xml.XMLUtil;
58 import org.w3c.dom.Document JavaDoc;
59 import org.w3c.dom.Element JavaDoc;
60 import org.w3c.dom.Node JavaDoc;
61 import org.w3c.dom.NodeList JavaDoc;
62 import org.xml.sax.InputSource JavaDoc;
63 import org.xml.sax.SAXException JavaDoc;
64
65 /**
66  * Support class for implementing Ant-based projects.
67  * @author Jesse Glick
68  */

69 public final class AntProjectHelper {
70     
71     /**
72      * Relative path from project directory to the customary shared properties file.
73      */

74     public static final String JavaDoc PROJECT_PROPERTIES_PATH = "nbproject/project.properties"; // NOI18N
75

76     /**
77      * Relative path from project directory to the customary private properties file.
78      */

79     public static final String JavaDoc PRIVATE_PROPERTIES_PATH = "nbproject/private/private.properties"; // NOI18N
80

81     /**
82      * Relative path from project directory to the required shared project metadata file.
83      */

84     public static final String JavaDoc PROJECT_XML_PATH = AntBasedProjectFactorySingleton.PROJECT_XML_PATH;
85     
86     /**
87      * Relative path from project directory to the required private project metadata file.
88      */

89     public static final String JavaDoc PRIVATE_XML_PATH = "nbproject/private/private.xml"; // NOI18N
90

91     /**
92      * XML namespace of Ant projects.
93      */

94     static final String JavaDoc PROJECT_NS = AntBasedProjectFactorySingleton.PROJECT_NS;
95     
96     /**
97      * XML namespace of private component of Ant projects.
98      */

99     static final String JavaDoc PRIVATE_NS = "http://www.netbeans.org/ns/project-private/1"; // NOI18N
100

101     static {
102         AntBasedProjectFactorySingleton.HELPER_CALLBACK = new AntBasedProjectFactorySingleton.AntProjectHelperCallback() {
103             public AntProjectHelper createHelper(FileObject dir, Document JavaDoc projectXml, ProjectState state, AntBasedProjectType type) {
104                 return new AntProjectHelper(dir, projectXml, state, type);
105             }
106             public void save(AntProjectHelper helper) throws IOException JavaDoc {
107                 helper.save();
108             }
109         };
110     }
111     
112     private static final RequestProcessor RP = new RequestProcessor("AntProjectHelper.RP"); // NOI18N
113

114     /**
115      * Project base directory.
116      */

117     private final FileObject dir;
118     
119     /**
120      * State object permitting modifications.
121      */

122     private final ProjectState state;
123     
124     /**
125      * Ant-based project type factory.
126      */

127     private final AntBasedProjectType type;
128     
129     /**
130      * Cached project.xml parse (null if not loaded).
131      * Access within {@link #modifiedMetadataPaths} monitor.
132      */

133     private Document JavaDoc projectXml;
134     
135     /**
136      * Cached private.xml parse (null if not loaded).
137      * Access within {@link #modifiedMetadataPaths} monitor.
138      */

139     private Document JavaDoc privateXml;
140     
141     /**
142      * Set of relative paths to metadata files which have been modified
143      * and which need to be saved.
144      * Also server as a monitor for {@link #projectXml} and {@link #privateXml} accesses;
145      * Xerces' DOM is not thread-safe <em>even for reading<em> (#50198).
146      */

147     private final Set JavaDoc<String JavaDoc> modifiedMetadataPaths = new HashSet JavaDoc<String JavaDoc>();
148
149     /**
150      * Task to remove cached metadata after some time, if it is not being actively used.
151      * @see "issue #90195"
152      */

153     private final RequestProcessor.Task clearUnmodifiedMetadataTask;
154     private static final int CLEAR_UNMODIFIED_METADATA_TIMEOUT = 15000;
155     
156     /**
157      * Registered listeners.
158      * Access must be directly synchronized.
159      */

160     private final List JavaDoc<AntProjectListener> listeners = new ArrayList JavaDoc<AntProjectListener>();
161     
162     /**
163      * List of loaded properties.
164      */

165     private final ProjectProperties properties;
166     
167     /** Listener to XML files; needs to be held as an instance field so it is not GC'd */
168     private final FileChangeSupportListener fileListener;
169     
170     /** True if currently saving XML files. */
171     private boolean writingXML = false;
172     
173     /**
174      * Hook waiting to be called. See issue #57794.
175      */

176     private ProjectXmlSavedHook pendingHook;
177     /**
178      * Number of metadata files remaining to be written before {@link #pendingHook} can be called.
179      * Javadoc for {@link ProjectXmlSavedHook} only guarantees that project.xml will be written,
180      * but best to be safe and make sure also private.xml and *.properties are too.
181      */

182     private int pendingHookCount;
183     
184     // XXX lock any loaded XML files while the project is modified, to prevent manual editing,
185
// and reload any modified files if the project is unmodified
186

187     private AntProjectHelper(FileObject dir, Document JavaDoc projectXml, ProjectState state, AntBasedProjectType type) {
188         this.dir = dir;
189         assert dir != null && FileUtil.toFile(dir) != null;
190         this.state = state;
191         assert state != null;
192         this.type = type;
193         assert type != null;
194         this.projectXml = projectXml;
195         assert projectXml != null;
196         properties = new ProjectProperties(this);
197         fileListener = new FileListener();
198         FileChangeSupport.DEFAULT.addListener(fileListener, resolveFile(PROJECT_XML_PATH));
199         FileChangeSupport.DEFAULT.addListener(fileListener, resolveFile(PRIVATE_XML_PATH));
200         clearUnmodifiedMetadataTask = RP.post(new Runnable JavaDoc() {
201             public void run() {
202                 synchronized (modifiedMetadataPaths) {
203                     if (modifiedMetadataPaths.isEmpty()) {
204                         AntProjectHelper.this.projectXml = null;
205                         privateXml = null;
206                         properties.clear();
207                     }
208                 }
209             }
210         }, CLEAR_UNMODIFIED_METADATA_TIMEOUT);
211     }
212     
213     /**
214      * Get the corresponding Ant-based project type factory.
215      */

216     AntBasedProjectType getType() {
217         return type;
218     }
219
220     /**
221      * Retrieve project.xml or private.xml, loading from disk as needed.
222      * private.xml is created as a skeleton on demand.
223      */

224     private Document JavaDoc getConfigurationXml(boolean shared) {
225         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
226         assert Thread.holdsLock(modifiedMetadataPaths);
227         clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
228         Document JavaDoc xml = shared ? projectXml : privateXml;
229         if (xml == null) {
230             String JavaDoc path = shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH;
231             xml = loadXml(path);
232             if (xml == null) {
233                 // Missing or broken; create a skeleton.
234
String JavaDoc element = shared ? "project" : "project-private"; // NOI18N
235
String JavaDoc ns = shared ? PROJECT_NS : PRIVATE_NS;
236                 xml = XMLUtil.createDocument(element, ns, null, null);
237                 if (shared) {
238                     // #46048: need to generate minimal compliant XML skeleton.
239
Element JavaDoc typeEl = xml.createElementNS(PROJECT_NS, "type"); // NOI18N
240
typeEl.appendChild(xml.createTextNode(getType().getType()));
241                     xml.getDocumentElement().appendChild(typeEl);
242                     xml.getDocumentElement().appendChild(xml.createElementNS(PROJECT_NS, "configuration")); // NOI18N
243
}
244             }
245             if (shared) {
246                 projectXml = xml;
247             } else {
248                 privateXml = xml;
249             }
250         }
251         assert xml != null;
252         return xml;
253     }
254     
255     /**
256      * If true, do not report XML load errors.
257      * For use only by unit tests.
258      */

259     static boolean QUIETLY_SWALLOW_XML_LOAD_ERRORS = false;
260     
261     /**
262      * Try to load a config XML file from a named path.
263      * If the file does not exist, or there is any load error, return null.
264      */

265     private Document JavaDoc loadXml(String JavaDoc path) {
266         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
267         assert Thread.holdsLock(modifiedMetadataPaths);
268         FileObject xml = dir.getFileObject(path);
269         if (xml == null || !xml.isData()) {
270             return null;
271         }
272         File JavaDoc f = FileUtil.toFile(xml);
273         assert f != null;
274         try {
275             return XMLUtil.parse(new InputSource JavaDoc(f.toURI().toString()), false, true, Util.defaultErrorHandler(), null);
276         } catch (IOException JavaDoc e) {
277             if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) {
278                 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
279             }
280         } catch (SAXException JavaDoc e) {
281             if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) {
282                 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
283             }
284         }
285         return null;
286     }
287     
288     /**
289      * Save an XML config file to a named path.
290      * If the file does not yet exist, it is created.
291      */

292     private FileLock saveXml(final Document JavaDoc doc, final String JavaDoc path) throws IOException JavaDoc {
293         assert ProjectManager.mutex().isWriteAccess();
294         assert !writingXML;
295         assert Thread.holdsLock(modifiedMetadataPaths);
296         final FileLock[] _lock = new FileLock[1];
297         writingXML = true;
298         try {
299             dir.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
300                 public void run() throws IOException JavaDoc {
301                     // Keep a copy of xml *while holding modifiedMetadataPaths monitor*.
302
ByteArrayOutputStream JavaDoc baos = new ByteArrayOutputStream JavaDoc();
303                     XMLUtil.write(doc, baos, "UTF-8"); // NOI18N
304
final byte[] data = baos.toByteArray();
305                     final FileObject xml = FileUtil.createData(dir, path);
306                     try {
307                         _lock[0] = xml.lock(); // unlocked by {@link #save}
308
OutputStream JavaDoc os = xml.getOutputStream(_lock[0]);
309                         try {
310                             os.write(data);
311                         } finally {
312                             os.close();
313                         }
314                     } catch (UserQuestionException uqe) { // #46089
315
needPendingHook();
316                         UserQuestionHandler.handle(uqe, new UserQuestionHandler.Callback() {
317                             public void accepted() {
318                                 // Try again.
319
assert !writingXML;
320                                 writingXML = true;
321                                 try {
322                                     FileLock lock = xml.lock();
323                                     try {
324                                         OutputStream JavaDoc os = xml.getOutputStream(lock);
325                                         try {
326                                             os.write(data);
327                                         } finally {
328                                             os.close();
329                                         }
330                                     } finally {
331                                         lock.releaseLock();
332                                     }
333                                     maybeCallPendingHook();
334                                 } catch (IOException JavaDoc e) {
335                                     // Oh well.
336
ErrorManager.getDefault().notify(e);
337                                     reload();
338                                 } finally {
339                                     writingXML = false;
340                                 }
341                             }
342                             public void denied() {
343                                 reload();
344                             }
345                             public void error(IOException JavaDoc e) {
346                                 ErrorManager.getDefault().notify(e);
347                                 reload();
348                             }
349                             private void reload() {
350                                 // Revert the save.
351
if (path.equals(PROJECT_XML_PATH)) {
352                                     synchronized (modifiedMetadataPaths) {
353                                         projectXml = null;
354                                     }
355                                 } else {
356                                     assert path.equals(PRIVATE_XML_PATH) : path;
357                                     synchronized (modifiedMetadataPaths) {
358                                         privateXml = null;
359                                     }
360                                 }
361                                 clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
362                                 fireExternalChange(path);
363                                 cancelPendingHook();
364                             }
365                         });
366                     }
367                 }
368             });
369         } finally {
370             writingXML = false;
371         }
372         return _lock[0];
373     }
374     
375     /**
376      * Get the <code>&lt;configuration&gt;</code> element of project.xml
377      * or the document element of private.xml.
378      * Beneath this point you can load and store configuration fragments.
379      * @param shared if true, use project.xml, else private.xml
380      * @return the data root
381      */

382     private Element JavaDoc getConfigurationDataRoot(boolean shared) {
383         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
384         assert Thread.holdsLock(modifiedMetadataPaths);
385         Document JavaDoc doc = getConfigurationXml(shared);
386         if (shared) {
387             Element JavaDoc project = doc.getDocumentElement();
388             Element JavaDoc config = Util.findElement(project, "configuration", PROJECT_NS); // NOI18N
389
assert config != null;
390             return config;
391         } else {
392             return doc.getDocumentElement();
393         }
394     }
395
396     /**
397      * Add a listener to changes in the project configuration.
398      * <p>Thread-safe.
399      * @param listener a listener to add
400      */

401     public void addAntProjectListener(AntProjectListener listener) {
402         synchronized (listeners) {
403             listeners.add(listener);
404         }
405     }
406     
407     /**
408      * Remove a listener to changes in the project configuration.
409      * <p>Thread-safe.
410      * @param listener a listener to remove
411      */

412     public void removeAntProjectListener(AntProjectListener listener) {
413         synchronized (listeners) {
414             listeners.remove(listener);
415         }
416     }
417     
418     /**
419      * Fire a change of external provenance to all listeners.
420      * Acquires write access.
421      * @param path path to the changed file (XML or properties)
422      */

423     void fireExternalChange(final String JavaDoc path) {
424         final Mutex.Action<Void JavaDoc> action = new Mutex.Action<Void JavaDoc>() {
425             public Void JavaDoc run() {
426                 fireChange(path, false);
427                 return null;
428             }
429         };
430         if (ProjectManager.mutex().isWriteAccess()) {
431             // Run it right now. postReadRequest would be too late.
432
ProjectManager.mutex().readAccess(action);
433         } else if (ProjectManager.mutex().isReadAccess()) {
434             // Run immediately also. No need to switch to read access.
435
action.run();
436         } else {
437             // Not safe to acquire a new lock, so run later in read access.
438
RP.post(new Runnable JavaDoc() {
439                 public void run() {
440                     ProjectManager.mutex().readAccess(action);
441                 }
442             });
443         }
444     }
445
446     /**
447      * Fire a change to all listeners.
448      * Must be called from write access; enters read access while firing.
449      * @param path path to the changed file (XML or properties)
450      * @param expected true if the result of an API-initiated change, false if from external causes
451      */

452     private void fireChange(String JavaDoc path, boolean expected) {
453         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
454         final AntProjectListener[] _listeners;
455         synchronized (listeners) {
456             if (listeners.isEmpty()) {
457                 return;
458             }
459             _listeners = listeners.toArray(new AntProjectListener[listeners.size()]);
460         }
461         final AntProjectEvent ev = new AntProjectEvent(this, path, expected);
462         final boolean xml = path.equals(PROJECT_XML_PATH) || path.equals(PRIVATE_XML_PATH);
463         ProjectManager.mutex().readAccess(new Mutex.Action<Void JavaDoc>() {
464             public Void JavaDoc run() {
465                 for (AntProjectListener l : _listeners) {
466                     try {
467                         if (xml) {
468                             l.configurationXmlChanged(ev);
469                         } else {
470                             l.propertiesChanged(ev);
471                         }
472                     } catch (RuntimeException JavaDoc e) {
473                         // Don't prevent other listeners from being notified.
474
ErrorManager.getDefault().notify(e);
475                     }
476                 }
477                 return null;
478             }
479         });
480     }
481     
482     /**
483      * Call when explicitly modifying some piece of metadata.
484      */

485     private void modifying(String JavaDoc path) {
486         assert ProjectManager.mutex().isWriteAccess();
487         state.markModified();
488         synchronized (modifiedMetadataPaths) {
489             modifiedMetadataPaths.add(path);
490         }
491         fireChange(path, true);
492     }
493     
494     /**
495      * Get the top-level project directory.
496      * @return the project directory beneath which everything in the project lies
497      */

498     public FileObject getProjectDirectory() {
499         return dir;
500     }
501     
502     /**Notification that this project has been deleted.
503      * @see org.netbeans.spi.project.ProjectState#notifyDeleted
504      *
505      * @since 1.8
506      */

507     public void notifyDeleted() {
508         state.notifyDeleted();
509     }
510     
511     
512     /**
513      * Mark this project as being modified without actually changing anything in it.
514      * Should only be called from {@link ProjectGenerator#createProject}.
515      */

516     void markModified() {
517         assert ProjectManager.mutex().isWriteAccess();
518         state.markModified();
519         // To make sure projectXmlSaved is called:
520
synchronized (modifiedMetadataPaths) {
521             modifiedMetadataPaths.add(PROJECT_XML_PATH);
522         }
523     }
524     
525     /**
526      * Check whether this project is currently modified including modifications
527      * to <code>project.xml</code>.
528      * Access from GeneratedFilesHelper.
529      */

530     boolean isProjectXmlModified() {
531         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
532         return modifiedMetadataPaths.contains(PROJECT_XML_PATH);
533     }
534     
535     /**
536      * Save all cached project metadata.
537      * If <code>project.xml</code> was one of the modified files, then
538      * {@link AntBasedProjectType#projectXmlSaved} is called, presumably
539      * creating <code>build-impl.xml</code> and/or <code>build.xml</code>.
540      */

541     private void save() throws IOException JavaDoc {
542         assert ProjectManager.mutex().isWriteAccess();
543         Set JavaDoc<FileLock> locks = new HashSet JavaDoc<FileLock>();
544         try {
545             synchronized (modifiedMetadataPaths) {
546                 assert !modifiedMetadataPaths.isEmpty();
547                 assert pendingHook == null;
548                 if (modifiedMetadataPaths.contains(PROJECT_XML_PATH)) {
549                     // Saving project.xml so look for that hook.
550
Project p = AntBasedProjectFactorySingleton.getProjectFor(this);
551                     pendingHook = p.getLookup().lookup(ProjectXmlSavedHook.class);
552                     // might still be null
553
}
554                 Iterator JavaDoc<String JavaDoc> it = modifiedMetadataPaths.iterator();
555                 while (it.hasNext()) {
556                     String JavaDoc path = it.next();
557                     if (path.equals(PROJECT_XML_PATH)) {
558                         assert projectXml != null;
559                         locks.add(saveXml(projectXml, path));
560                     } else if (path.equals(PRIVATE_XML_PATH)) {
561                         assert privateXml != null;
562                         locks.add(saveXml(privateXml, path));
563                     } else {
564                         // All else is assumed to be a properties file.
565
locks.add(properties.write(path));
566                     }
567                     // As metadata files are saved, take them off the modified list.
568
it.remove();
569                 }
570                 if (pendingHook != null && pendingHookCount == 0) {
571                     try {
572                         pendingHook.projectXmlSaved();
573                     } catch (IOException JavaDoc e) {
574                         // Treat it as still modified.
575
modifiedMetadataPaths.add(PROJECT_XML_PATH);
576                         throw e;
577                     }
578                 }
579                 clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
580             }
581         } finally {
582             // #57791: release locks outside synchronized block.
583
locks.remove(null);
584             for (FileLock lock : locks) {
585                 lock.releaseLock();
586             }
587             // More #57794.
588
if (pendingHookCount == 0) {
589                 pendingHook = null;
590             }
591         }
592     }
593     
594     /** See issue #57794. */
595     void maybeCallPendingHook() {
596         // XXX synchronization of this method?
597
assert pendingHookCount > 0;
598         pendingHookCount--;
599         //#67465: the pendingHook may be null if project.xml is not being written
600
//eg. only project.properties is being saved:
601
if (pendingHookCount == 0 && pendingHook != null) {
602             try {
603                 ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void JavaDoc>() {
604                     public Void JavaDoc run() throws IOException JavaDoc {
605                         pendingHook.projectXmlSaved();
606                         return null;
607                     }
608                 });
609             } catch (MutexException e) {
610                 // XXX mark project modified again??
611
ErrorManager.getDefault().notify(e);
612             } finally {
613                 pendingHook = null;
614             }
615         }
616     }
617     void cancelPendingHook() {
618         assert pendingHookCount > 0;
619         pendingHookCount--;
620         if (pendingHookCount == 0) {
621             pendingHook = null;
622         }
623     }
624     void needPendingHook() {
625         pendingHookCount++;
626     }
627     
628     /**
629      * Load a property file from some location in the project.
630      * The returned object may be edited but you must call {@link #putProperties}
631      * to save any changes you make.
632      * If the file does not (yet) exist or could not be loaded for whatever reason,
633      * an empty properties list is returned instead.
634      * @param path a relative URI in the project directory, e.g.
635      * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
636      * @return a set of properties
637      */

638     public EditableProperties getProperties(final String JavaDoc path) {
639         if (path.equals(AntProjectHelper.PROJECT_XML_PATH) || path.equals(AntProjectHelper.PRIVATE_XML_PATH)) {
640             throw new IllegalArgumentException JavaDoc("Attempt to load properties from a project XML file"); // NOI18N
641
}
642         clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
643         return ProjectManager.mutex().readAccess(new Mutex.Action<EditableProperties>() {
644             public EditableProperties run() {
645                 return properties.getProperties(path);
646             }
647         });
648     }
649     
650     /**
651      * Store a property file to some location in the project.
652      * A clone will be made of the supplied properties file so as to snapshot it.
653      * The new properties are not actually stored to disk immediately, but the project
654      * is marked modified so that they will be later.
655      * You can store to a path that does not yet exist and the file will be created
656      * if and when the project is saved.
657      * If the old value is the same as the new, nothing is done.
658      * Otherwise an expected properties change event is fired.
659      * <p>Acquires write access from {@link ProjectManager#mutex}. However, you are well
660      * advised to explicitly enclose a <em>complete</em> operation within write access,
661      * starting with {@link #getProperties}, to prevent race conditions.
662      * @param path a relative URI in the project directory, e.g.
663      * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
664      * @param props a set of properties to store, or null to delete any existing properties file there
665      */

666     public void putProperties(final String JavaDoc path, final EditableProperties props) {
667         if (path.equals(AntProjectHelper.PROJECT_XML_PATH) || path.equals(AntProjectHelper.PRIVATE_XML_PATH)) {
668             throw new IllegalArgumentException JavaDoc("Attempt to store properties from a project XML file"); // NOI18N
669
}
670         clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
671         ProjectManager.mutex().writeAccess(new Mutex.Action<Void JavaDoc>() {
672             public Void JavaDoc run() {
673                 if (properties.putProperties(path, props)) {
674                     modifying(path);
675                 }
676                 return null;
677             }
678         });
679     }
680     
681     /**
682      * Get a property provider that works with loadable project properties.
683      * Its current values should match {@link #getProperties}, and calls to
684      * {@link #putProperties} should cause it to fire changes.
685      * @param path a relative URI in the project directory, e.g.
686      * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
687      * @return a property provider implementation
688      */

689     public PropertyProvider getPropertyProvider(final String JavaDoc path) {
690         clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
691         if (path.equals(AntProjectHelper.PROJECT_XML_PATH) || path.equals(AntProjectHelper.PRIVATE_XML_PATH)) {
692             throw new IllegalArgumentException JavaDoc("Attempt to store properties from a project XML file"); // NOI18N
693
}
694         return ProjectManager.mutex().readAccess(new Mutex.Action<PropertyProvider>() {
695             public PropertyProvider run() {
696                 return properties.getPropertyProvider(path);
697             }
698         });
699     }
700     
701     /**
702      * Get the primary configuration data for this project.
703      * The returned element will be named according to
704      * {@link AntBasedProjectType#getPrimaryConfigurationDataElementName} and
705      * {@link AntBasedProjectType#getPrimaryConfigurationDataElementNamespace}.
706      * The project may read this document fragment to get custom information
707      * from <code>nbproject/project.xml</code> and <code>nbproject/private/private.xml</code>.
708      * The fragment will have no parent node and while it may be modified, you must
709      * use {@link #putPrimaryConfigurationData} to store any changes.
710      * @param shared if true, refers to <code>project.xml</code>, else refers to
711      * <code>private.xml</code>
712      * @return the configuration data that is available
713      */

714     public Element JavaDoc getPrimaryConfigurationData(final boolean shared) {
715         final String JavaDoc name = type.getPrimaryConfigurationDataElementName(shared);
716         assert name.indexOf(':') == -1;
717         final String JavaDoc namespace = type.getPrimaryConfigurationDataElementNamespace(shared);
718         assert namespace != null && namespace.length() > 0;
719         return ProjectManager.mutex().readAccess(new Mutex.Action<Element JavaDoc>() {
720             public Element JavaDoc run() {
721                 synchronized (modifiedMetadataPaths) {
722                     Element JavaDoc el = getConfigurationFragment(name, namespace, shared);
723                     if (el != null) {
724                         return el;
725                     } else {
726                         // No such data, corrupt file.
727
return cloneSafely(getConfigurationXml(shared).createElementNS(namespace, name));
728                     }
729                 }
730             }
731         });
732     }
733     
734     /**
735      * Store the primary configuration data for this project.
736      * The supplied element must be named according to
737      * {@link AntBasedProjectType#getPrimaryConfigurationDataElementName} and
738      * {@link AntBasedProjectType#getPrimaryConfigurationDataElementNamespace}.
739      * The project may save this document fragment to set custom information
740      * in <code>nbproject/project.xml</code> and <code>nbproject/private/private.xml</code>.
741      * The fragment will be cloned and so further modifications will have no effect.
742      * <p>Acquires write access from {@link ProjectManager#mutex}. However, you are well
743      * advised to explicitly enclose a <em>complete</em> operation within write access,
744      * starting with {@link #getPrimaryConfigurationData}, to prevent race conditions.
745      * @param data the desired new configuration data
746      * @param shared if true, refers to <code>project.xml</code>, else refers to
747      * <code>private.xml</code>
748      * @throws IllegalArgumentException if the element is not correctly named
749      */

750     public void putPrimaryConfigurationData(Element JavaDoc data, boolean shared) throws IllegalArgumentException JavaDoc {
751         String JavaDoc name = type.getPrimaryConfigurationDataElementName(shared);
752         assert name.indexOf(':') == -1;
753         String JavaDoc namespace = type.getPrimaryConfigurationDataElementNamespace(shared);
754         assert namespace != null && namespace.length() > 0;
755         if (!name.equals(data.getLocalName()) || !namespace.equals(data.getNamespaceURI())) {
756             throw new IllegalArgumentException JavaDoc("Wrong name/namespace: expected {" + namespace + "}" + name + " but was {" + data.getNamespaceURI() + "}" + data.getLocalName()); // NOI18N
757
}
758         putConfigurationFragment(data, shared);
759     }
760     
761     private final class FileListener implements FileChangeSupportListener {
762         
763         public FileListener() {}
764         
765         private void change(File JavaDoc f) {
766             if (writingXML) {
767                 return;
768             }
769             String JavaDoc path;
770             synchronized (modifiedMetadataPaths) {
771                 if (f.equals(resolveFile(PROJECT_XML_PATH))) {
772                     if (modifiedMetadataPaths.contains(PROJECT_XML_PATH)) {
773                         //#68872: don't do anything if the given file has non-saved changes:
774
return ;
775                     }
776                     path = PROJECT_XML_PATH;
777                     projectXml = null;
778                 } else if (f.equals(resolveFile(PRIVATE_XML_PATH))) {
779                     if (modifiedMetadataPaths.contains(PRIVATE_XML_PATH)) {
780                         //#68872: don't do anything if the given file has non-saved changes:
781
return ;
782                     }
783                     path = PRIVATE_XML_PATH;
784                     privateXml = null;
785                 } else {
786                     throw new AssertionError JavaDoc("Unexpected file change in " + f); // NOI18N
787
}
788             }
789             fireExternalChange(path);
790         }
791         
792         public void fileCreated(FileChangeSupportEvent event) {
793             change(event.getPath());
794         }
795         
796         public void fileDeleted(FileChangeSupportEvent event) {
797             change(event.getPath());
798         }
799         
800         public void fileModified(FileChangeSupportEvent event) {
801             change(event.getPath());
802         }
803         
804     }
805     
806     /**
807      * Get a piece of the configuration subtree by name.
808      * @param elementName the simple XML element name expected
809      * @param namespace the XML namespace expected
810      * @param shared to use project.xml vs. private.xml
811      * @return (a clone of) the named configuration fragment, or null if it does not exist
812      */

813     Element JavaDoc getConfigurationFragment(final String JavaDoc elementName, final String JavaDoc namespace, final boolean shared) {
814         return ProjectManager.mutex().readAccess(new Mutex.Action<Element JavaDoc>() {
815             public Element JavaDoc run() {
816                 synchronized (modifiedMetadataPaths) {
817                     Element JavaDoc root = getConfigurationDataRoot(shared);
818                     Element JavaDoc data = Util.findElement(root, elementName, namespace);
819                     if (data != null) {
820                         return cloneSafely(data);
821                     } else {
822                         return null;
823                     }
824                 }
825             }
826         });
827     }
828     
829     private static final DocumentBuilder JavaDoc db;
830     static {
831         try {
832             db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
833         } catch (ParserConfigurationException JavaDoc e) {
834             throw new AssertionError JavaDoc(e);
835         }
836     }
837     private static Element JavaDoc cloneSafely(Element JavaDoc el) {
838         // #50198: for thread safety, use a separate document.
839
// Using XMLUtil.createDocument is much too slow.
840
synchronized (db) {
841             Document JavaDoc dummy = db.newDocument();
842             return (Element JavaDoc) dummy.importNode(el, true);
843         }
844     }
845     
846     /**
847      * Store a piece of the configuration subtree by name.
848      * @param fragment a piece of the subtree to store (overwrite or add)
849      * @param shared to use project.xml vs. private.xml
850      */

851     void putConfigurationFragment(final Element JavaDoc fragment, final boolean shared) {
852         ProjectManager.mutex().writeAccess(new Mutex.Action<Void JavaDoc>() {
853             public Void JavaDoc run() {
854                 synchronized (modifiedMetadataPaths) {
855                     Element JavaDoc root = getConfigurationDataRoot(shared);
856                     Element JavaDoc existing = Util.findElement(root, fragment.getLocalName(), fragment.getNamespaceURI());
857                     // XXX first compare to existing and return if the same
858
if (existing != null) {
859                         root.removeChild(existing);
860                     }
861                     // the children are alphabetize: find correct place to insert new node
862
Node JavaDoc ref = null;
863                     NodeList JavaDoc list = root.getChildNodes();
864                     for (int i=0; i<list.getLength(); i++) {
865                         Node JavaDoc node = list.item(i);
866                         if (node.getNodeType() != Node.ELEMENT_NODE) {
867                             continue;
868                         }
869                         int comparison = node.getNodeName().compareTo(fragment.getNodeName());
870                         if (comparison == 0) {
871                             comparison = node.getNamespaceURI().compareTo(fragment.getNamespaceURI());
872                         }
873                         if (comparison > 0) {
874                             ref = node;
875                             break;
876                         }
877                     }
878                     root.insertBefore(root.getOwnerDocument().importNode(fragment, true), ref);
879                     modifying(shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH);
880                 }
881                 return null;
882             }
883         });
884     }
885     
886     /**
887      * Remove a piece of the configuration subtree by name.
888      * @param elementName the simple XML element name expected
889      * @param namespace the XML namespace expected
890      * @param shared to use project.xml vs. private.xml
891      * @return true if anything was actually removed
892      */

893     boolean removeConfigurationFragment(final String JavaDoc elementName, final String JavaDoc namespace, final boolean shared) {
894         return ProjectManager.mutex().writeAccess(new Mutex.Action<Boolean JavaDoc>() {
895             public Boolean JavaDoc run() {
896                 synchronized (modifiedMetadataPaths) {
897                     Element JavaDoc root = getConfigurationDataRoot(shared);
898                     Element JavaDoc data = Util.findElement(root, elementName, namespace);
899                     if (data != null) {
900                         root.removeChild(data);
901                         modifying(shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH);
902                         return true;
903                     } else {
904                         return false;
905                     }
906                 }
907             }
908         });
909     }
910     
911     /**
912      * Create an object permitting this project to store auxiliary configuration.
913      * Would be placed into the project's lookup.
914      * @return an auxiliary configuration provider object suitable for the project lookup
915      */

916     public AuxiliaryConfiguration createAuxiliaryConfiguration() {
917         return new ExtensibleMetadataProviderImpl(this);
918     }
919     
920     /**
921      * Create an object permitting this project to expose a cache directory.
922      * Would be placed into the project's lookup.
923      * @return a cache directory provider object suitable for the project lookup
924      */

925     public CacheDirectoryProvider createCacheDirectoryProvider() {
926         return new ExtensibleMetadataProviderImpl(this);
927     }
928     
929     /**
930      * Create an implementation of {@link org.netbeans.api.queries.FileBuiltQuery} that works with files
931      * within the project based on simple glob pattern mappings.
932      * <p>
933      * It is intended to be
934      * placed in {@link org.netbeans.api.project.Project#getLookup}.
935      * <p>
936      * It will return status objects for any files in the project matching a source
937      * glob pattern - this must include exactly one asterisk (<code>*</code>)
938      * representing a variable portion of a source file path (always slash-separated
939      * and relative to the project directory) and may include some Ant property
940      * references which will be resolved as per the property evaluator.
941      * A file is considered out of date if there is no file represented by the
942      * matching target pattern (which has the same format), or the target file is older
943      * than the source file, or the source file is modified as per
944      * {@link org.openide.loaders.DataObject#isModified}.
945      * An attempt is made to fire changes from the status object whenever the result
946      * should change from one call to the next.
947      * <p>
948      * The (evaluated) source and target patterns may be relative, resolved against
949      * the project directory (perhaps going outside it), or absolute.
950      * </p>
951      * <div class="nonnormative">
952      * <p>
953      * A typical set of source and target patterns would be:
954      * </p>
955      * <ol>
956      * <li><samp>${src.dir}/*.java</samp>
957      * <li><samp>${test.src.dir}/*.java</samp>
958      * </ol>
959      * <ol>
960      * <li><samp>${build.classes.dir}/*.class</samp>
961      * <li><samp>${test.build.classes.dir}/*.class</samp>
962      * </ol>
963      * </div>
964      * @param eval a property evaluator to interpret the patterns with
965      * @param from a list of glob patterns for source files
966      * @param to a matching list of glob patterns for built files
967      * @return a query implementation
968      * @throws IllegalArgumentException if either from or to patterns
969      * have zero or multiple asterisks,
970      * or the arrays are not of equal lengths
971      */

972     public FileBuiltQueryImplementation createGlobFileBuiltQuery(PropertyEvaluator eval, String JavaDoc[] from, String JavaDoc[] to) throws IllegalArgumentException JavaDoc {
973         return new GlobFileBuiltQuery(this, eval, from, to);
974     }
975
976     /**
977      * Create a basic implementation of {@link AntArtifact} which assumes everything of interest
978      * is in a fixed location under a standard Ant-based project.
979      * @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>
980      * @param locationProperty an Ant property name giving the project-relative
981      * location of the artifact, e.g. <samp>dist.jar</samp>
982      * @param eval a way to evaluate the location property (e.g. {@link #getStandardPropertyEvaluator})
983      * @param targetName the name of an Ant target which will build the artifact,
984      * e.g. <samp>jar</samp>
985      * @param cleanTargetName the name of an Ant target which will delete the artifact
986      * (and maybe other build products), e.g. <samp>clean</samp>
987      * @return an artifact
988      */

989     public AntArtifact createSimpleAntArtifact(String JavaDoc type, String JavaDoc locationProperty, PropertyEvaluator eval, String JavaDoc targetName, String JavaDoc cleanTargetName) {
990         return new SimpleAntArtifact(this, type, locationProperty, eval, targetName, cleanTargetName);
991     }
992     
993     /**
994      * Create an implementation of the file sharability query.
995      * You may specify a list of source roots to include that should be considered sharable,
996      * as well as a list of build directories that should not be considered sharable.
997      * <p>
998      * The project directory itself is automatically included in the list of sharable directories
999      * so you need not explicitly specify it.
1000     * Similarly, the <code>nbproject/private</code> subdirectory is automatically excluded
1001     * from VCS, so you do not need to explicitly specify it.
1002     * </p>
1003     * <p>
1004     * Any file (or directory) mentioned (explicitly or implicity) in the source
1005     * directory list but not in any of the build directory lists, and not containing
1006     * any build directories inside it, will be given as sharable. If a directory itself
1007     * is sharable but some directory inside it is not, it will be given as mixed.
1008     * A file or directory inside some build directory will be listed as not sharable.
1009     * A file or directory matching neither the source list nor the build directory list
1010     * will be treated as of unknown status, but in practice such a file should never
1011     * have been passed to this implementation anyway - {@link org.netbeans.api.queries.SharabilityQuery} will
1012     * normally only call an implementation in project lookup if the file is owned by
1013     * that project.
1014     * </p>
1015     * <p>
1016     * Each entry in either list should be a string evaluated first for Ant property
1017     * escapes (if any), then treated as a file path relative to the project directory
1018     * (or it may be absolute).
1019     * </p>
1020     * <p>
1021     * It is permitted, and harmless, to include items that overlap others. For example,
1022     * you can have both a directory and one of its children in the include list.
1023     * </p>
1024     * <div class="nonnormative">
1025     * <p>
1026     * Typical usage would be:
1027     * </p>
1028     * <pre>
1029     * helper.createSharabilityQuery(helper.getStandardPropertyEvaluator(),
1030     * new String[] {"${src.dir}", "${test.src.dir}"},
1031     * new String[] {"${build.dir}", "${dist.dir}"})
1032     * </pre>
1033     * <p>
1034     * A quick rule of thumb is that the include list should contain any
1035     * source directories which <em>might</em> reside outside the project directory;
1036     * and the exclude list should contain any directories which you would want
1037     * to add to a <samp>.cvsignore</samp> file if using CVS (for example).
1038     * </p>
1039     * <p>
1040     * Note that in this case <samp>${src.dir}</samp> and <samp>${test.src.dir}</samp>
1041     * may be relative paths inside the project directory; relative paths pointing
1042     * outside of the project directory; or absolute paths (generally outside of the
1043     * project directory). If they refer to locations inside the project directory,
1044     * including them does nothing but is harmless - since the project directory itself
1045     * is always treated as sharable. If they refer to external locations, you will
1046     * need to also make sure that {@link org.netbeans.api.queries.FileOwnerQuery} actually maps files in those
1047     * directories to this project, or else {@link org.netbeans.api.queries.SharabilityQuery} will never find
1048     * this implementation in your project lookup and may return <code>UNKNOWN</code>.
1049     * </p>
1050     * </div>
1051     * @param eval a property evaluator to interpret paths with
1052     * @param sourceRoots a list of additional paths to treat as sharable
1053     * @param buildDirectories a list of paths to treat as not sharable
1054     * @return a sharability query implementation suitable for the project lookup
1055     * @see Project#getLookup
1056     */

1057    public SharabilityQueryImplementation createSharabilityQuery(PropertyEvaluator eval, String JavaDoc[] sourceRoots, String JavaDoc[] buildDirectories) {
1058        String JavaDoc[] includes = new String JavaDoc[sourceRoots.length + 1];
1059        System.arraycopy(sourceRoots, 0, includes, 0, sourceRoots.length);
1060        includes[sourceRoots.length] = ""; // NOI18N
1061
String JavaDoc[] excludes = new String JavaDoc[buildDirectories.length + 1];
1062        System.arraycopy(buildDirectories, 0, excludes, 0, buildDirectories.length);
1063        excludes[buildDirectories.length] = "nbproject/private"; // NOI18N
1064
return new SharabilityQueryImpl(this, eval, includes, excludes);
1065    }
1066    
1067    /**
1068     * Get a property provider which defines <code>basedir</code> according to
1069     * the project directory and also copies all system properties in the current VM.
1070     * It may also define <code>ant.home</code> if it is able.
1071     * @return a stock property provider for initial Ant-related definitions
1072     * @see PropertyUtils#sequentialPropertyEvaluator
1073     */

1074    public PropertyProvider getStockPropertyPreprovider() {
1075        clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
1076        return properties.getStockPropertyPreprovider();
1077    }
1078    
1079    /**
1080     * Get a property evaluator that can evaluate properties according to the default
1081     * file layout for Ant-based projects.
1082     * First, {@link #getStockPropertyPreprovider stock properties} are predefined.
1083     * Then {@link #PRIVATE_PROPERTIES_PATH} is loaded via {@link #getPropertyProvider},
1084     * then global definitions from {@link PropertyUtils#globalPropertyProvider}
1085     * (though these may be overridden using the property <code>user.properties.file</code>
1086     * in <code>private.properties</code>), then {@link #PROJECT_PROPERTIES_PATH}.
1087     * @return a standard property evaluator
1088     */

1089    public PropertyEvaluator getStandardPropertyEvaluator() {
1090        clearUnmodifiedMetadataTask.schedule(CLEAR_UNMODIFIED_METADATA_TIMEOUT);
1091        return properties.getStandardPropertyEvaluator();
1092    }
1093    
1094    /**
1095     * Find an absolute file path from a possibly project-relative path.
1096     * @param filename a pathname which may be project-relative or absolute and may
1097     * use / or \ as the path separator
1098     * @return an absolute file corresponding to it
1099     */

1100    public File JavaDoc resolveFile(String JavaDoc filename) {
1101        if (filename == null) {
1102            throw new NullPointerException JavaDoc("Attempted to pass a null filename to resolveFile"); // NOI18N
1103
}
1104        return PropertyUtils.resolveFile(FileUtil.toFile(dir), filename);
1105    }
1106    
1107    /**
1108     * Same as {@link #resolveFile}, but produce a <code>FileObject</code> if possible.
1109     * @param filename a pathname according to Ant conventions
1110     * @return a file object it represents, or null if there is no such file object in known filesystems
1111     */

1112    public FileObject resolveFileObject(String JavaDoc filename) {
1113        if (filename == null) {
1114            throw new NullPointerException JavaDoc("Must pass a non-null filename"); // NOI18N
1115
}
1116        return PropertyUtils.resolveFileObject(dir, filename);
1117    }
1118    
1119    /**
1120     * Take an Ant-style path specification and convert it to a platform-specific absolute path.
1121     * The path separator characters are converted to the local convention, and individual
1122     * path components are resolved and cleaned up as for {@link #resolveFile}.
1123     * @param path an Ant-style abstract path
1124     * @return an absolute, locally usable path
1125     */

1126    public String JavaDoc resolvePath(String JavaDoc path) {
1127        if (path == null) {
1128            throw new NullPointerException JavaDoc("Must pass a non-null path"); // NOI18N
1129
}
1130        // XXX consider memoizing results since this is probably called a lot
1131
return PropertyUtils.resolvePath(FileUtil.toFile(dir), path);
1132    }
1133    
1134    public String JavaDoc toString() {
1135        return "AntProjectHelper[" + getProjectDirectory() + "]"; // NOI18N
1136
}
1137
1138}
1139
Popular Tags