KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > netbeans > modules > ruby > spi > project > support > rake > RakeProjectHelper


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.modules.ruby.spi.project.support.rake;
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.modules.ruby.api.project.rake.RakeArtifact;
37 import org.netbeans.modules.ruby.modules.project.rake.RakeBasedProjectFactorySingleton;
38 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupport;
39 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupportEvent;
40 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupportListener;
41 import org.netbeans.modules.ruby.modules.project.rake.UserQuestionHandler;
42 import org.netbeans.modules.ruby.modules.project.rake.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 RakeProjectHelper {
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 = RakeBasedProjectFactorySingleton.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 = RakeBasedProjectFactorySingleton.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         RakeBasedProjectFactorySingleton.HELPER_CALLBACK = new RakeBasedProjectFactorySingleton.RakeProjectHelperCallback() {
103             public RakeProjectHelper createHelper(FileObject dir, Document JavaDoc projectXml, ProjectState state, RakeBasedProjectType type) {
104                 return new RakeProjectHelper(dir, projectXml, state, type);
105             }
106             public void save(RakeProjectHelper helper) throws IOException JavaDoc {
107                 helper.save();
108             }
109         };
110     }
111     
112     private static final RequestProcessor RP = new RequestProcessor("RakeProjectHelper.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 RakeBasedProjectType 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      * Registered listeners.
151      * Access must be directly synchronized.
152      */

153     private final List JavaDoc<RakeProjectListener> listeners = new ArrayList JavaDoc<RakeProjectListener>();
154     
155     /**
156      * List of loaded properties.
157      */

158     private final ProjectProperties properties;
159     
160     /** Listener to XML files; needs to be held as an instance field so it is not GC'd */
161     private final FileChangeSupportListener fileListener;
162     
163     /** True if currently saving XML files. */
164     private boolean writingXML = false;
165     
166     /**
167      * Hook waiting to be called. See issue #57794.
168      */

169     private ProjectXmlSavedHook pendingHook;
170     /**
171      * Number of metadata files remaining to be written before {@link #pendingHook} can be called.
172      * Javadoc for {@link ProjectXmlSavedHook} only guarantees that project.xml will be written,
173      * but best to be safe and make sure also private.xml and *.properties are too.
174      */

175     private int pendingHookCount;
176     
177     // XXX lock any loaded XML files while the project is modified, to prevent manual editing,
178
// and reload any modified files if the project is unmodified
179

180     private RakeProjectHelper(FileObject dir, Document JavaDoc projectXml, ProjectState state, RakeBasedProjectType type) {
181         this.dir = dir;
182         assert dir != null && FileUtil.toFile(dir) != null;
183         this.state = state;
184         assert state != null;
185         this.type = type;
186         assert type != null;
187         this.projectXml = projectXml;
188         assert projectXml != null;
189         properties = new ProjectProperties(this);
190         fileListener = new FileListener();
191         FileChangeSupport.DEFAULT.addListener(fileListener, resolveFile(PROJECT_XML_PATH));
192         FileChangeSupport.DEFAULT.addListener(fileListener, resolveFile(PRIVATE_XML_PATH));
193     }
194     
195     /**
196      * Get the corresponding Ant-based project type factory.
197      */

198     RakeBasedProjectType getType() {
199         return type;
200     }
201
202     /**
203      * Retrieve project.xml or private.xml, loading from disk as needed.
204      * private.xml is created as a skeleton on demand.
205      */

206     private Document JavaDoc getConfigurationXml(boolean shared) {
207         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
208         assert Thread.holdsLock(modifiedMetadataPaths);
209         Document JavaDoc xml = shared ? projectXml : privateXml;
210         if (xml == null) {
211             String JavaDoc path = shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH;
212             xml = loadXml(path);
213             if (xml == null) {
214                 // Missing or broken; create a skeleton.
215
String JavaDoc element = shared ? "project" : "project-private"; // NOI18N
216
String JavaDoc ns = shared ? PROJECT_NS : PRIVATE_NS;
217                 xml = XMLUtil.createDocument(element, ns, null, null);
218                 if (shared) {
219                     // #46048: need to generate minimal compliant XML skeleton.
220
Element JavaDoc typeEl = xml.createElementNS(PROJECT_NS, "type"); // NOI18N
221
typeEl.appendChild(xml.createTextNode(getType().getType()));
222                     xml.getDocumentElement().appendChild(typeEl);
223                     xml.getDocumentElement().appendChild(xml.createElementNS(PROJECT_NS, "configuration")); // NOI18N
224
}
225             }
226             if (shared) {
227                 projectXml = xml;
228             } else {
229                 privateXml = xml;
230             }
231         }
232         assert xml != null;
233         return xml;
234     }
235     
236     /**
237      * If true, do not report XML load errors.
238      * For use only by unit tests.
239      */

240     static boolean QUIETLY_SWALLOW_XML_LOAD_ERRORS = false;
241     
242     /**
243      * Try to load a config XML file from a named path.
244      * If the file does not exist, or there is any load error, return null.
245      */

246     private Document JavaDoc loadXml(String JavaDoc path) {
247         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
248         assert Thread.holdsLock(modifiedMetadataPaths);
249         FileObject xml = dir.getFileObject(path);
250         if (xml == null || !xml.isData()) {
251             return null;
252         }
253         File JavaDoc f = FileUtil.toFile(xml);
254         assert f != null;
255         try {
256             return XMLUtil.parse(new InputSource JavaDoc(f.toURI().toString()), false, true, Util.defaultErrorHandler(), null);
257         } catch (IOException JavaDoc e) {
258             if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) {
259                 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
260             }
261         } catch (SAXException JavaDoc e) {
262             if (!QUIETLY_SWALLOW_XML_LOAD_ERRORS) {
263                 ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
264             }
265         }
266         return null;
267     }
268     
269     /**
270      * Save an XML config file to a named path.
271      * If the file does not yet exist, it is created.
272      */

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

362     private Element JavaDoc getConfigurationDataRoot(boolean shared) {
363         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
364         assert Thread.holdsLock(modifiedMetadataPaths);
365         Document JavaDoc doc = getConfigurationXml(shared);
366         if (shared) {
367             Element JavaDoc project = doc.getDocumentElement();
368             Element JavaDoc config = Util.findElement(project, "configuration", PROJECT_NS); // NOI18N
369
assert config != null;
370             return config;
371         } else {
372             return doc.getDocumentElement();
373         }
374     }
375
376     /**
377      * Add a listener to changes in the project configuration.
378      * <p>Thread-safe.
379      * @param listener a listener to add
380      */

381     public void addRakeProjectListener(RakeProjectListener listener) {
382         synchronized (listeners) {
383             listeners.add(listener);
384         }
385     }
386     
387     /**
388      * Remove a listener to changes in the project configuration.
389      * <p>Thread-safe.
390      * @param listener a listener to remove
391      */

392     public void removeRakeProjectListener(RakeProjectListener listener) {
393         synchronized (listeners) {
394             listeners.remove(listener);
395         }
396     }
397     
398     /**
399      * Fire a change of external provenance to all listeners.
400      * Acquires write access.
401      * @param path path to the changed file (XML or properties)
402      */

403     void fireExternalChange(final String JavaDoc path) {
404         final Mutex.Action<Void JavaDoc> action = new Mutex.Action<Void JavaDoc>() {
405             public Void JavaDoc run() {
406                 fireChange(path, false);
407                 return null;
408             }
409         };
410         if (ProjectManager.mutex().isWriteAccess()) {
411             // Run it right now. postReadRequest would be too late.
412
ProjectManager.mutex().readAccess(action);
413         } else if (ProjectManager.mutex().isReadAccess()) {
414             // Run immediately also. No need to switch to read access.
415
action.run();
416         } else {
417             // Not safe to acquire a new lock, so run later in read access.
418
RP.post(new Runnable JavaDoc() {
419                 public void run() {
420                     ProjectManager.mutex().readAccess(action);
421                 }
422             });
423         }
424     }
425
426     /**
427      * Fire a change to all listeners.
428      * Must be called from write access; enters read access while firing.
429      * @param path path to the changed file (XML or properties)
430      * @param expected true if the result of an API-initiated change, false if from external causes
431      */

432     private void fireChange(String JavaDoc path, boolean expected) {
433         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
434         final RakeProjectListener[] _listeners;
435         synchronized (listeners) {
436             if (listeners.isEmpty()) {
437                 return;
438             }
439             _listeners = listeners.toArray(new RakeProjectListener[listeners.size()]);
440         }
441         final RakeProjectEvent ev = new RakeProjectEvent(this, path, expected);
442         final boolean xml = path.equals(PROJECT_XML_PATH) || path.equals(PRIVATE_XML_PATH);
443         ProjectManager.mutex().readAccess(new Mutex.Action<Void JavaDoc>() {
444             public Void JavaDoc run() {
445                 for (int i = 0; i < _listeners.length; i++) {
446                     try {
447                         if (xml) {
448                             _listeners[i].configurationXmlChanged(ev);
449                         } else {
450                             _listeners[i].propertiesChanged(ev);
451                         }
452                     } catch (RuntimeException JavaDoc e) {
453                         // Don't prevent other listeners from being notified.
454
ErrorManager.getDefault().notify(e);
455                     }
456                 }
457                 return null;
458             }
459         });
460     }
461     
462     /**
463      * Call when explicitly modifying some piece of metadata.
464      */

465     private void modifying(String JavaDoc path) {
466         assert ProjectManager.mutex().isWriteAccess();
467         state.markModified();
468         synchronized (modifiedMetadataPaths) {
469             modifiedMetadataPaths.add(path);
470         }
471         fireChange(path, true);
472     }
473     
474     /**
475      * Get the top-level project directory.
476      * @return the project directory beneath which everything in the project lies
477      */

478     public FileObject getProjectDirectory() {
479         return dir;
480     }
481     
482     /**Notification that this project has been deleted.
483      * @see org.netbeans.spi.project.ProjectState#notifyDeleted
484      *
485      * @since 1.8
486      */

487     public void notifyDeleted() {
488         state.notifyDeleted();
489     }
490     
491     
492     /**
493      * Mark this project as being modified without actually changing anything in it.
494      * Should only be called from {@link ProjectGenerator#createProject}.
495      */

496     void markModified() {
497         assert ProjectManager.mutex().isWriteAccess();
498         state.markModified();
499         // To make sure projectXmlSaved is called:
500
synchronized (modifiedMetadataPaths) {
501             modifiedMetadataPaths.add(PROJECT_XML_PATH);
502         }
503     }
504     
505     /**
506      * Check whether this project is currently modified including modifications
507      * to <code>project.xml</code>.
508      * Access from GeneratedFilesHelper.
509      */

510     boolean isProjectXmlModified() {
511         assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
512         return modifiedMetadataPaths.contains(PROJECT_XML_PATH);
513     }
514     
515     /**
516      * Save all cached project metadata.
517      * If <code>project.xml</code> was one of the modified files, then
518      * {@link RakeBasedProjectType#projectXmlSaved} is called, presumably
519      * creating <code>build-impl.xml</code> and/or <code>build.xml</code>.
520      */

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

618     public EditableProperties getProperties(final String JavaDoc path) {
619         if (path.equals(RakeProjectHelper.PROJECT_XML_PATH) || path.equals(RakeProjectHelper.PRIVATE_XML_PATH)) {
620             throw new IllegalArgumentException JavaDoc("Attempt to load properties from a project XML file"); // NOI18N
621
}
622         return ProjectManager.mutex().readAccess(new Mutex.Action<EditableProperties>() {
623             public EditableProperties run() {
624                 return properties.getProperties(path);
625             }
626         });
627     }
628     
629     /**
630      * Store a property file to some location in the project.
631      * A clone will be made of the supplied properties file so as to snapshot it.
632      * The new properties are not actually stored to disk immediately, but the project
633      * is marked modified so that they will be later.
634      * You can store to a path that does not yet exist and the file will be created
635      * if and when the project is saved.
636      * If the old value is the same as the new, nothing is done.
637      * Otherwise an expected properties change event is fired.
638      * <p>Acquires write access from {@link ProjectManager#mutex}. However, you are well
639      * advised to explicitly enclose a <em>complete</em> operation within write access,
640      * starting with {@link #getProperties}, to prevent race conditions.
641      * @param path a relative URI in the project directory, e.g.
642      * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
643      * @param props a set of properties to store, or null to delete any existing properties file there
644      */

645     public void putProperties(final String JavaDoc path, final EditableProperties props) {
646         if (path.equals(RakeProjectHelper.PROJECT_XML_PATH) || path.equals(RakeProjectHelper.PRIVATE_XML_PATH)) {
647             throw new IllegalArgumentException JavaDoc("Attempt to store properties from a project XML file"); // NOI18N
648
}
649         ProjectManager.mutex().writeAccess(new Mutex.Action<Void JavaDoc>() {
650             public Void JavaDoc run() {
651                 if (properties.putProperties(path, props)) {
652                     modifying(path);
653                 }
654                 return null;
655             }
656         });
657     }
658     
659     /**
660      * Get a property provider that works with loadable project properties.
661      * Its current values should match {@link #getProperties}, and calls to
662      * {@link #putProperties} should cause it to fire changes.
663      * @param path a relative URI in the project directory, e.g.
664      * {@link #PROJECT_PROPERTIES_PATH} or {@link #PRIVATE_PROPERTIES_PATH}
665      * @return a property provider implementation
666      */

667     public PropertyProvider getPropertyProvider(final String JavaDoc path) {
668         if (path.equals(RakeProjectHelper.PROJECT_XML_PATH) || path.equals(RakeProjectHelper.PRIVATE_XML_PATH)) {
669             throw new IllegalArgumentException JavaDoc("Attempt to store properties from a project XML file"); // NOI18N
670
}
671         return ProjectManager.mutex().readAccess(new Mutex.Action<PropertyProvider>() {
672             public PropertyProvider run() {
673                 return properties.getPropertyProvider(path);
674             }
675         });
676     }
677     
678     /**
679      * Get the primary configuration data for this project.
680      * The returned element will be named according to
681      * {@link RakeBasedProjectType#getPrimaryConfigurationDataElementName} and
682      * {@link RakeBasedProjectType#getPrimaryConfigurationDataElementNamespace}.
683      * The project may read this document fragment to get custom information
684      * from <code>nbproject/project.xml</code> and <code>nbproject/private/private.xml</code>.
685      * The fragment will have no parent node and while it may be modified, you must
686      * use {@link #putPrimaryConfigurationData} to store any changes.
687      * @param shared if true, refers to <code>project.xml</code>, else refers to
688      * <code>private.xml</code>
689      * @return the configuration data that is available
690      */

691     public Element JavaDoc getPrimaryConfigurationData(final boolean shared) {
692         final String JavaDoc name = type.getPrimaryConfigurationDataElementName(shared);
693         assert name.indexOf(':') == -1;
694         final String JavaDoc namespace = type.getPrimaryConfigurationDataElementNamespace(shared);
695         assert namespace != null && namespace.length() > 0;
696         return ProjectManager.mutex().readAccess(new Mutex.Action<Element JavaDoc>() {
697             public Element JavaDoc run() {
698                 synchronized (modifiedMetadataPaths) {
699                     Element JavaDoc el = getConfigurationFragment(name, namespace, shared);
700                     if (el != null) {
701                         return el;
702                     } else {
703                         // No such data, corrupt file.
704
return cloneSafely(getConfigurationXml(shared).createElementNS(namespace, name));
705                     }
706                 }
707             }
708         });
709     }
710     
711     /**
712      * Store the primary configuration data for this project.
713      * The supplied element must be named according to
714      * {@link RakeBasedProjectType#getPrimaryConfigurationDataElementName} and
715      * {@link RakeBasedProjectType#getPrimaryConfigurationDataElementNamespace}.
716      * The project may save this document fragment to set custom information
717      * in <code>nbproject/project.xml</code> and <code>nbproject/private/private.xml</code>.
718      * The fragment will be cloned and so further modifications will have no effect.
719      * <p>Acquires write access from {@link ProjectManager#mutex}. However, you are well
720      * advised to explicitly enclose a <em>complete</em> operation within write access,
721      * starting with {@link #getPrimaryConfigurationData}, to prevent race conditions.
722      * @param data the desired new configuration data
723      * @param shared if true, refers to <code>project.xml</code>, else refers to
724      * <code>private.xml</code>
725      * @throws IllegalArgumentException if the element is not correctly named
726      */

727     public void putPrimaryConfigurationData(Element JavaDoc data, boolean shared) throws IllegalArgumentException JavaDoc {
728         String JavaDoc name = type.getPrimaryConfigurationDataElementName(shared);
729         assert name.indexOf(':') == -1;
730         String JavaDoc namespace = type.getPrimaryConfigurationDataElementNamespace(shared);
731         assert namespace != null && namespace.length() > 0;
732         if (!name.equals(data.getLocalName()) || !namespace.equals(data.getNamespaceURI())) {
733             throw new IllegalArgumentException JavaDoc("Wrong name/namespace: expected {" + namespace + "}" + name + " but was {" + data.getNamespaceURI() + "}" + data.getLocalName()); // NOI18N
734
}
735         putConfigurationFragment(data, shared);
736     }
737     
738     private final class FileListener implements FileChangeSupportListener {
739         
740         public FileListener() {}
741         
742         private void change(File JavaDoc f) {
743             if (writingXML) {
744                 return;
745             }
746             String JavaDoc path;
747             synchronized (modifiedMetadataPaths) {
748                 if (f.equals(resolveFile(PROJECT_XML_PATH))) {
749                     if (modifiedMetadataPaths.contains(PROJECT_XML_PATH)) {
750                         //#68872: don't do anything if the given file has non-saved changes:
751
return ;
752                     }
753                     path = PROJECT_XML_PATH;
754                     projectXml = null;
755                 } else if (f.equals(resolveFile(PRIVATE_XML_PATH))) {
756                     if (modifiedMetadataPaths.contains(PRIVATE_XML_PATH)) {
757                         //#68872: don't do anything if the given file has non-saved changes:
758
return ;
759                     }
760                     path = PRIVATE_XML_PATH;
761                     privateXml = null;
762                 } else {
763                     throw new AssertionError JavaDoc("Unexpected file change in " + f); // NOI18N
764
}
765             }
766             fireExternalChange(path);
767         }
768         
769         public void fileCreated(FileChangeSupportEvent event) {
770             change(event.getPath());
771         }
772         
773         public void fileDeleted(FileChangeSupportEvent event) {
774             change(event.getPath());
775         }
776         
777         public void fileModified(FileChangeSupportEvent event) {
778             change(event.getPath());
779         }
780         
781     }
782     
783     /**
784      * Get a piece of the configuration subtree by name.
785      * @param elementName the simple XML element name expected
786      * @param namespace the XML namespace expected
787      * @param shared to use project.xml vs. private.xml
788      * @return (a clone of) the named configuration fragment, or null if it does not exist
789      */

790     Element JavaDoc getConfigurationFragment(final String JavaDoc elementName, final String JavaDoc namespace, final boolean shared) {
791         return ProjectManager.mutex().readAccess(new Mutex.Action<Element JavaDoc>() {
792             public Element JavaDoc run() {
793                 synchronized (modifiedMetadataPaths) {
794                     Element JavaDoc root = getConfigurationDataRoot(shared);
795                     Element JavaDoc data = Util.findElement(root, elementName, namespace);
796                     if (data != null) {
797                         return cloneSafely(data);
798                     } else {
799                         return null;
800                     }
801                 }
802             }
803         });
804     }
805     
806     private static final DocumentBuilder JavaDoc db;
807     static {
808         try {
809             db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
810         } catch (ParserConfigurationException JavaDoc e) {
811             throw new AssertionError JavaDoc(e);
812         }
813     }
814     private static Element JavaDoc cloneSafely(Element JavaDoc el) {
815         // #50198: for thread safety, use a separate document.
816
// Using XMLUtil.createDocument is much too slow.
817
synchronized (db) {
818             Document JavaDoc dummy = db.newDocument();
819             return (Element JavaDoc) dummy.importNode(el, true);
820         }
821     }
822     
823     /**
824      * Store a piece of the configuration subtree by name.
825      * @param fragment a piece of the subtree to store (overwrite or add)
826      * @param shared to use project.xml vs. private.xml
827      */

828     void putConfigurationFragment(final Element JavaDoc fragment, final boolean shared) {
829         ProjectManager.mutex().writeAccess(new Mutex.Action<Void JavaDoc>() {
830             public Void JavaDoc run() {
831                 synchronized (modifiedMetadataPaths) {
832                     Element JavaDoc root = getConfigurationDataRoot(shared);
833                     Element JavaDoc existing = Util.findElement(root, fragment.getLocalName(), fragment.getNamespaceURI());
834                     // XXX first compare to existing and return if the same
835
if (existing != null) {
836                         root.removeChild(existing);
837                     }
838                     // the children are alphabetize: find correct place to insert new node
839
Node JavaDoc ref = null;
840                     NodeList JavaDoc list = root.getChildNodes();
841                     for (int i=0; i<list.getLength(); i++) {
842                         Node JavaDoc node = list.item(i);
843                         if (node.getNodeType() != Node.ELEMENT_NODE) {
844                             continue;
845                         }
846                         int comparison = node.getNodeName().compareTo(fragment.getNodeName());
847                         if (comparison == 0) {
848                             comparison = node.getNamespaceURI().compareTo(fragment.getNamespaceURI());
849                         }
850                         if (comparison > 0) {
851                             ref = node;
852                             break;
853                         }
854                     }
855                     root.insertBefore(root.getOwnerDocument().importNode(fragment, true), ref);
856                     modifying(shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH);
857                 }
858                 return null;
859             }
860         });
861     }
862     
863     /**
864      * Remove a piece of the configuration subtree by name.
865      * @param elementName the simple XML element name expected
866      * @param namespace the XML namespace expected
867      * @param shared to use project.xml vs. private.xml
868      * @return true if anything was actually removed
869      */

870     boolean removeConfigurationFragment(final String JavaDoc elementName, final String JavaDoc namespace, final boolean shared) {
871         return ProjectManager.mutex().writeAccess(new Mutex.Action<Boolean JavaDoc>() {
872             public Boolean JavaDoc run() {
873                 synchronized (modifiedMetadataPaths) {
874                     Element JavaDoc root = getConfigurationDataRoot(shared);
875                     Element JavaDoc data = Util.findElement(root, elementName, namespace);
876                     if (data != null) {
877                         root.removeChild(data);
878                         modifying(shared ? PROJECT_XML_PATH : PRIVATE_XML_PATH);
879                         return true;
880                     } else {
881                         return false;
882                     }
883                 }
884             }
885         });
886     }
887     
888     /**
889      * Create an object permitting this project to store auxiliary configuration.
890      * Would be placed into the project's lookup.
891      * @return an auxiliary configuration provider object suitable for the project lookup
892      */

893     public AuxiliaryConfiguration createAuxiliaryConfiguration() {
894         return new ExtensibleMetadataProviderImpl(this);
895     }
896     
897     /**
898      * Create an object permitting this project to expose a cache directory.
899      * Would be placed into the project's lookup.
900      * @return a cache directory provider object suitable for the project lookup
901      */

902     public CacheDirectoryProvider createCacheDirectoryProvider() {
903         return new ExtensibleMetadataProviderImpl(this);
904     }
905     
906     /**
907      * Create a basic implementation of {@link RakeArtifact} which assumes everything of interest
908      * is in a fixed location under a standard Ant-based project.
909      * @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>
910      * @param locationProperty an Ant property name giving the project-relative
911      * location of the artifact, e.g. <samp>dist.jar</samp>
912      * @param eval a way to evaluate the location property (e.g. {@link #getStandardPropertyEvaluator})
913      * @param targetName the name of an Ant target which will build the artifact,
914      * e.g. <samp>jar</samp>
915      * @param cleanTargetName the name of an Ant target which will delete the artifact
916      * (and maybe other build products), e.g. <samp>clean</samp>
917      * @return an artifact
918      */

919     public RakeArtifact createSimpleRakeArtifact(String JavaDoc type, String JavaDoc locationProperty, PropertyEvaluator eval, String JavaDoc targetName, String JavaDoc cleanTargetName) {
920         return new SimpleRakeArtifact(this, type, locationProperty, eval, targetName, cleanTargetName);
921     }
922     
923     /**
924      * Create an implementation of the file sharability query.
925      * You may specify a list of source roots to include that should be considered sharable,
926      * as well as a list of build directories that should not be considered sharable.
927      * <p>
928      * The project directory itself is automatically included in the list of sharable directories
929      * so you need not explicitly specify it.
930      * Similarly, the <code>nbproject/private</code> subdirectory is automatically excluded
931      * from VCS, so you do not need to explicitly specify it.
932      * </p>
933      * <p>
934      * Any file (or directory) mentioned (explicitly or implicity) in the source
935      * directory list but not in any of the build directory lists, and not containing
936      * any build directories inside it, will be given as sharable. If a directory itself
937      * is sharable but some directory inside it is not, it will be given as mixed.
938      * A file or directory inside some build directory will be listed as not sharable.
939      * A file or directory matching neither the source list nor the build directory list
940      * will be treated as of unknown status, but in practice such a file should never
941      * have been passed to this implementation anyway - {@link org.netbeans.api.queries.SharabilityQuery} will
942      * normally only call an implementation in project lookup if the file is owned by
943      * that project.
944      * </p>
945      * <p>
946      * Each entry in either list should be a string evaluated first for Ant property
947      * escapes (if any), then treated as a file path relative to the project directory
948      * (or it may be absolute).
949      * </p>
950      * <p>
951      * It is permitted, and harmless, to include items that overlap others. For example,
952      * you can have both a directory and one of its children in the include list.
953      * </p>
954      * <div class="nonnormative">
955      * <p>
956      * Typical usage would be:
957      * </p>
958      * <pre>
959      * helper.createSharabilityQuery(helper.getStandardPropertyEvaluator(),
960      * new String[] {"${src.dir}", "${test.src.dir}"},
961      * new String[] {"${build.dir}", "${dist.dir}"})
962      * </pre>
963      * <p>
964      * A quick rule of thumb is that the include list should contain any
965      * source directories which <em>might</em> reside outside the project directory;
966      * and the exclude list should contain any directories which you would want
967      * to add to a <samp>.cvsignore</samp> file if using CVS (for example).
968      * </p>
969      * <p>
970      * Note that in this case <samp>${src.dir}</samp> and <samp>${test.src.dir}</samp>
971      * may be relative paths inside the project directory; relative paths pointing
972      * outside of the project directory; or absolute paths (generally outside of the
973      * project directory). If they refer to locations inside the project directory,
974      * including them does nothing but is harmless - since the project directory itself
975      * is always treated as sharable. If they refer to external locations, you will
976      * need to also make sure that {@link org.netbeans.api.queries.FileOwnerQuery} actually maps files in those
977      * directories to this project, or else {@link org.netbeans.api.queries.SharabilityQuery} will never find
978      * this implementation in your project lookup and may return <code>UNKNOWN</code>.
979      * </p>
980      * </div>
981      * @param eval a property evaluator to interpret paths with
982      * @param sourceRoots a list of additional paths to treat as sharable
983      * @param buildDirectories a list of paths to treat as not sharable
984      * @return a sharability query implementation suitable for the project lookup
985      * @see Project#getLookup
986      */

987     public SharabilityQueryImplementation createSharabilityQuery(PropertyEvaluator eval, String JavaDoc[] sourceRoots, String JavaDoc[] buildDirectories) {
988         String JavaDoc[] includes = new String JavaDoc[sourceRoots.length + 1];
989         System.arraycopy(sourceRoots, 0, includes, 0, sourceRoots.length);
990         includes[sourceRoots.length] = ""; // NOI18N
991
String JavaDoc[] excludes = new String JavaDoc[buildDirectories.length + 1];
992         System.arraycopy(buildDirectories, 0, excludes, 0, buildDirectories.length);
993         excludes[buildDirectories.length] = "nbproject/private"; // NOI18N
994
return new SharabilityQueryImpl(this, eval, includes, excludes);
995     }
996     
997     /**
998      * Get a property provider which defines <code>basedir</code> according to
999      * the project directory and also copies all system properties in the current VM.
1000     * It may also define <code>ant.home</code> if it is able.
1001     * @return a stock property provider for initial Ant-related definitions
1002     * @see PropertyUtils#sequentialPropertyEvaluator
1003     */

1004    public PropertyProvider getStockPropertyPreprovider() {
1005        return properties.getStockPropertyPreprovider();
1006    }
1007    
1008    /**
1009     * Get a property evaluator that can evaluate properties according to the default
1010     * file layout for Ant-based projects.
1011     * First, {@link #getStockPropertyPreprovider stock properties} are predefined.
1012     * Then {@link #PRIVATE_PROPERTIES_PATH} is loaded via {@link #getPropertyProvider},
1013     * then global definitions from {@link PropertyUtils#globalPropertyProvider}
1014     * (though these may be overridden using the property <code>user.properties.file</code>
1015     * in <code>private.properties</code>), then {@link #PROJECT_PROPERTIES_PATH}.
1016     * @return a standard property evaluator
1017     */

1018    public PropertyEvaluator getStandardPropertyEvaluator() {
1019        return properties.getStandardPropertyEvaluator();
1020    }
1021    
1022    /**
1023     * Find an absolute file path from a possibly project-relative path.
1024     * @param filename a pathname which may be project-relative or absolute and may
1025     * use / or \ as the path separator
1026     * @return an absolute file corresponding to it
1027     */

1028    public File JavaDoc resolveFile(String JavaDoc filename) {
1029        if (filename == null) {
1030            throw new NullPointerException JavaDoc("Attempted to pass a null filename to resolveFile"); // NOI18N
1031
}
1032        return PropertyUtils.resolveFile(FileUtil.toFile(dir), filename);
1033    }
1034    
1035    /**
1036     * Same as {@link #resolveFile}, but produce a <code>FileObject</code> if possible.
1037     * @param filename a pathname according to Ant conventions
1038     * @return a file object it represents, or null if there is no such file object in known filesystems
1039     */

1040    public FileObject resolveFileObject(String JavaDoc filename) {
1041        if (filename == null) {
1042            throw new NullPointerException JavaDoc("Must pass a non-null filename"); // NOI18N
1043
}
1044        return PropertyUtils.resolveFileObject(dir, filename);
1045    }
1046    
1047    /**
1048     * Take an Ant-style path specification and convert it to a platform-specific absolute path.
1049     * The path separator characters are converted to the local convention, and individual
1050     * path components are resolved and cleaned up as for {@link #resolveFile}.
1051     * @param path an Ant-style abstract path
1052     * @return an absolute, locally usable path
1053     */

1054    public String JavaDoc resolvePath(String JavaDoc path) {
1055        if (path == null) {
1056            throw new NullPointerException JavaDoc("Must pass a non-null path"); // NOI18N
1057
}
1058        // XXX consider memoizing results since this is probably called a lot
1059
return PropertyUtils.resolvePath(FileUtil.toFile(dir), path);
1060    }
1061    
1062    public String JavaDoc toString() {
1063        return "RakeProjectHelper[" + getProjectDirectory() + "]"; // NOI18N
1064
}
1065
1066}
1067
Popular Tags