KickJava   Java API By Example, From Geeks To Geeks.

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


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.beans.PropertyChangeEvent JavaDoc;
23 import java.beans.PropertyChangeListener JavaDoc;
24 import java.io.File JavaDoc;
25 import java.io.IOException JavaDoc;
26 import java.net.URL JavaDoc;
27 import java.util.ArrayList JavaDoc;
28 import java.util.HashMap JavaDoc;
29 import java.util.HashSet JavaDoc;
30 import java.util.Iterator JavaDoc;
31 import java.util.LinkedHashMap JavaDoc;
32 import java.util.List JavaDoc;
33 import java.util.Map JavaDoc;
34 import java.util.Set JavaDoc;
35 import javax.swing.Icon JavaDoc;
36 import javax.swing.event.ChangeEvent JavaDoc;
37 import javax.swing.event.ChangeListener JavaDoc;
38 import org.netbeans.api.project.FileOwnerQuery;
39 import org.netbeans.api.project.Project;
40 import org.netbeans.api.project.ProjectManager;
41 import org.netbeans.api.project.ProjectUtils;
42 import org.netbeans.api.project.SourceGroup;
43 import org.netbeans.api.project.Sources;
44 import org.netbeans.modules.ruby.modules.project.rake.RakeBasedProjectFactorySingleton;
45 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupport;
46 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupportEvent;
47 import org.netbeans.modules.ruby.modules.project.rake.FileChangeSupportListener;
48 import org.netbeans.spi.project.support.GenericSources;
49 import org.openide.filesystems.FileObject;
50 import org.openide.filesystems.FileStateInvalidException;
51 import org.openide.filesystems.FileUtil;
52 import org.openide.util.WeakListeners;
53
54 // XXX should perhaps be legal to call add* methods at any time (should update things)
55
// and perhaps also have remove* methods
56
// and have code names for each source dir?
57

58 // XXX should probably all be wrapped in ProjectManager.mutex
59

60 /**
61  * Helper class to work with source roots and typed folders of a project.
62  * @author Jesse Glick
63  */

64 public final class SourcesHelper {
65     
66     private class Root {
67         protected final String JavaDoc location;
68         public Root(String JavaDoc location) {
69             this.location = location;
70         }
71         public final File JavaDoc getActualLocation() {
72             String JavaDoc val = evaluator.evaluate(location);
73             if (val == null) {
74                 return null;
75             }
76             return project.resolveFile(val);
77         }
78     }
79     
80     private class SourceRoot extends Root {
81         private final String JavaDoc displayName;
82         private final Icon JavaDoc icon;
83         private final Icon JavaDoc openedIcon;
84         public SourceRoot(String JavaDoc location, String JavaDoc displayName, Icon JavaDoc icon, Icon JavaDoc openedIcon) {
85             super(location);
86             this.displayName = displayName;
87             this.icon = icon;
88             this.openedIcon = openedIcon;
89         }
90         public final SourceGroup toGroup(FileObject loc) {
91             assert loc != null;
92             return GenericSources.group(getProject(), loc, location.length() > 0 ? location : "generic", // NOI18N
93
displayName, icon, openedIcon);
94         }
95     }
96     
97     private final class TypedSourceRoot extends SourceRoot {
98         private final String JavaDoc type;
99         public TypedSourceRoot(String JavaDoc type, String JavaDoc location, String JavaDoc displayName, Icon JavaDoc icon, Icon JavaDoc openedIcon) {
100             super(location, displayName, icon, openedIcon);
101             this.type = type;
102         }
103         public final String JavaDoc getType() {
104             return type;
105         }
106     }
107     
108     private final RakeProjectHelper project;
109     private final PropertyEvaluator evaluator;
110     private final List JavaDoc<SourceRoot> principalSourceRoots = new ArrayList JavaDoc<SourceRoot>();
111     private final List JavaDoc<Root> nonSourceRoots = new ArrayList JavaDoc<Root>();
112     private final List JavaDoc<TypedSourceRoot> typedSourceRoots = new ArrayList JavaDoc<TypedSourceRoot>();
113     private int registeredRootAlgorithm;
114     /**
115      * If not null, external roots that we registered the last time.
116      * Used when a property change is encountered, to see if the set of external
117      * roots might have changed. Hold the actual files (not e.g. URLs); see
118      * {@link #registerExternalRoots} for the reason why.
119      */

120     private Set JavaDoc<FileObject> lastRegisteredRoots;
121     private PropertyChangeListener JavaDoc propChangeL;
122     
123     /**
124      * Create the helper object, initially configured to recognize only sources
125      * contained inside the project directory.
126      * @param project an Ant project helper
127      * @param evaluator a way to evaluate Ant properties used to define source locations
128      */

129     public SourcesHelper(RakeProjectHelper project, PropertyEvaluator evaluator) {
130         this.project = project;
131         this.evaluator = evaluator;
132     }
133     
134     /**
135      * Add a possible principal source root, or top-level folder which may
136      * contain sources that should be considered part of the project.
137      * <p>
138      * If the actual value of the location is inside the project directory,
139      * this is simply ignored; so it safe to configure principal source roots
140      * for any source directory which might be set to use an external path, even
141      * if the common location is internal.
142      * </p>
143      * @param location a project-relative or absolute path giving the location
144      * of a source tree; may contain Ant property substitutions
145      * @param displayName a display name (for {@link SourceGroup#getDisplayName})
146      * @param icon a regular icon for the source root, or null
147      * @param openedIcon an opened variant icon for the source root, or null
148      * @throws IllegalStateException if this method is called after either
149      * {@link #createSources} or {@link #registerExternalRoots}
150      * was called
151      * @see #registerExternalRoots
152      * @see Sources#TYPE_GENERIC
153      */

154     public void addPrincipalSourceRoot(String JavaDoc location, String JavaDoc displayName, Icon JavaDoc icon, Icon JavaDoc openedIcon) throws IllegalStateException JavaDoc {
155         if (lastRegisteredRoots != null) {
156             throw new IllegalStateException JavaDoc("registerExternalRoots was already called"); // NOI18N
157
}
158         principalSourceRoots.add(new SourceRoot(location, displayName, icon, openedIcon));
159     }
160     
161     /**
162      * Similar to {@link #addPrincipalSourceRoot} but affects only
163      * {@link #registerExternalRoots} and not {@link #createSources}.
164      * <p class="nonnormative">
165      * Useful for project type providers which have external paths holding build
166      * products. These should not appear in {@link Sources}, yet it may be useful
167      * for {@link FileOwnerQuery} to know the owning project (for example, in order
168      * for a project-specific {@link org.netbeans.spi.queries.SourceForBinaryQueryImplementation} to work).
169      * </p>
170      * @param location a project-relative or absolute path giving the location
171      * of a non-source tree; may contain Ant property substitutions
172      * @throws IllegalStateException if this method is called after
173      * {@link #registerExternalRoots} was called
174      */

175     public void addNonSourceRoot(String JavaDoc location) throws IllegalStateException JavaDoc {
176         if (lastRegisteredRoots != null) {
177             throw new IllegalStateException JavaDoc("registerExternalRoots was already called"); // NOI18N
178
}
179         nonSourceRoots.add(new Root(location));
180     }
181     
182     /**
183      * Add a typed source root which will be considered only in certain contexts.
184      * @param location a project-relative or absolute path giving the location
185      * of a source tree; may contain Ant property substitutions
186      * @param type a source root type such as <a HREF="@JAVA/PROJECT@/org/netbeans/api/java/project/JavaProjectConstants.html#SOURCES_TYPE_JAVA"><code>JavaProjectConstants.SOURCES_TYPE_JAVA</code></a>
187      * @param displayName a display name (for {@link SourceGroup#getDisplayName})
188      * @param icon a regular icon for the source root, or null
189      * @param openedIcon an opened variant icon for the source root, or null
190      * @throws IllegalStateException if this method is called after either
191      * {@link #createSources} or {@link #registerExternalRoots}
192      * was called
193      */

194     public void addTypedSourceRoot(String JavaDoc location, String JavaDoc type, String JavaDoc displayName, Icon JavaDoc icon, Icon JavaDoc openedIcon) throws IllegalStateException JavaDoc {
195         if (lastRegisteredRoots != null) {
196             throw new IllegalStateException JavaDoc("registerExternalRoots was already called"); // NOI18N
197
}
198         typedSourceRoots.add(new TypedSourceRoot(type, location, displayName, icon, openedIcon));
199     }
200     
201     private Project getProject() {
202         return RakeBasedProjectFactorySingleton.getProjectFor(project);
203     }
204     
205     /**
206      * Register all external source or non-source roots using {@link FileOwnerQuery#markExternalOwner}.
207      * <p>
208      * Only roots added by {@link #addPrincipalSourceRoot} and {@link #addNonSourceRoot}
209      * are considered. They are registered if (and only if) they in fact fall
210      * outside of the project directory, and of course only if the folders really
211      * exist on disk. Currently it is not defined when this file existence check
212      * is done (e.g. when this method is first called, or periodically) or whether
213      * folders which are created subsequently will be registered, so project type
214      * providers are encouraged to create all desired external roots before calling
215      * this method.
216      * </p>
217      * <p>
218      * If the actual value of the location changes (due to changes being
219      * fired from the property evaluator), roots which were previously internal
220      * and are now external will be registered, and roots which were previously
221      * external and are now internal will be unregistered. The (un-)registration
222      * will be done using the same algorithm as was used initially.
223      * </p>
224      * <p>
225      * Calling this method causes the helper object to hold strong references to the
226      * current external roots, which helps a project satisfy the requirements of
227      * {@link FileOwnerQuery#EXTERNAL_ALGORITHM_TRANSIENT}.
228      * </p>
229      * <p>
230      * You may <em>not</em> call this method inside the project's constructor, as
231      * it requires the actual project to exist and be registered in {@link ProjectManager}.
232      * Typically you would use {@link org.openide.util.Mutex#postWriteRequest} to run it
233      * later, if you were creating the helper in your constructor, since the project construction
234      * normally occurs in read access.
235      * </p>
236      * @param algorithm an external root registration algorithm as per
237      * {@link FileOwnerQuery#markExternalOwner}
238      * @throws IllegalArgumentException if the algorithm is unrecognized
239      * @throws IllegalStateException if this method is called more than once on a
240      * given <code>SourcesHelper</code> object
241      */

242     public void registerExternalRoots(int algorithm) throws IllegalArgumentException JavaDoc, IllegalStateException JavaDoc {
243         if (lastRegisteredRoots != null) {
244             throw new IllegalStateException JavaDoc("registerExternalRoots was already called before"); // NOI18N
245
}
246         registeredRootAlgorithm = algorithm;
247         remarkExternalRoots();
248     }
249     
250     private void remarkExternalRoots() throws IllegalArgumentException JavaDoc {
251         List JavaDoc<Root> allRoots = new ArrayList JavaDoc<Root>(principalSourceRoots);
252         allRoots.addAll(nonSourceRoots);
253         Project p = getProject();
254         FileObject pdir = project.getProjectDirectory();
255         // First time: register roots and add to lastRegisteredRoots.
256
// Subsequent times: add to newRootsToRegister and maybe add them later.
257
Set JavaDoc<FileObject> newRootsToRegister;
258         if (lastRegisteredRoots == null) {
259             // First time.
260
newRootsToRegister = null;
261             lastRegisteredRoots = new HashSet JavaDoc<FileObject>();
262             propChangeL = new PropChangeL(); // hold a strong ref
263
evaluator.addPropertyChangeListener(WeakListeners.propertyChange(propChangeL, evaluator));
264         } else {
265             newRootsToRegister = new HashSet JavaDoc<FileObject>();
266         }
267         // XXX might be a bit more efficient to cache for each root the actualLocation value
268
// that was last computed, and just check if that has changed... otherwise we wind
269
// up calling APH.resolveFileObject repeatedly (for each property change)
270
for (Root r : allRoots) {
271             File JavaDoc locF = r.getActualLocation();
272             FileObject loc = locF != null ? FileUtil.toFileObject(locF) : null;
273             if (loc == null) {
274                 // Not there; skip it.
275
continue;
276             }
277             if (!loc.isFolder()) {
278                 // Actually a file. Skip it.
279
continue;
280             }
281             if (FileUtil.getRelativePath(pdir, loc) != null) {
282                 // Inside projdir already. Skip it.
283
continue;
284             }
285             try {
286                 Project other = ProjectManager.getDefault().findProject(loc);
287                 if (other != null) {
288                     // This is a foreign project; we cannot own it. Skip it.
289
continue;
290                 }
291             } catch (IOException JavaDoc e) {
292                 // Assume it is a foreign project and skip it.
293
continue;
294             }
295             // It's OK to go.
296
if (newRootsToRegister != null) {
297                 newRootsToRegister.add(loc);
298             } else {
299                 lastRegisteredRoots.add(loc);
300                 FileOwnerQuery.markExternalOwner(loc, p, registeredRootAlgorithm);
301             }
302         }
303         if (newRootsToRegister != null) {
304             // Just check for changes since the last time.
305
Set JavaDoc<FileObject> toUnregister = new HashSet JavaDoc<FileObject>(lastRegisteredRoots);
306             toUnregister.removeAll(newRootsToRegister);
307             for (FileObject loc : toUnregister) {
308                 FileOwnerQuery.markExternalOwner(loc, null, registeredRootAlgorithm);
309             }
310             newRootsToRegister.removeAll(lastRegisteredRoots);
311             for (FileObject loc : newRootsToRegister) {
312                 FileOwnerQuery.markExternalOwner(loc, p, registeredRootAlgorithm);
313             }
314         }
315     }
316
317     /**
318      * Create a source list object.
319      * <p>
320      * All principal source roots are listed as {@link Sources#TYPE_GENERIC} unless they
321      * are inside the project directory. The project directory itself is also listed
322      * (with a display name according to {@link ProjectUtils#getInformation}), unless
323      * it is contained by an explicit principal source root (i.e. ancestor directory).
324      * Principal source roots should never overlap; if two configured
325      * principal source roots are determined to have the same root folder, the first
326      * configured root takes precedence (which only matters in regard to the display
327      * name); if one root folder is contained within another, the broader
328      * root folder subsumes the narrower one so only the broader root is listed.
329      * </p>
330      * <p>
331      * Other source groups are listed according to the named typed source roots.
332      * There is no check performed that these do not overlap (though a project type
333      * provider should for UI reasons avoid this situation).
334      * </p>
335      * <p>
336      * Any source roots which do not exist on disk are ignored, as if they had
337      * not been configured at all. Currently it is not defined when this existence
338      * check is performed (e.g. when this method is called, when the source root
339      * is first accessed, periodically, etc.), so project type providers are
340      * generally encouraged to make sure all desired source folders exist
341      * before calling this method, if creating a new project.
342      * </p>
343      * <p>
344      * Source groups are created according to the semantics described in
345      * {@link GenericSources#group}. They are listed in the order they
346      * were configured (for those roots that are actually used as groups).
347      * </p>
348      * <p>
349      * You may call this method inside the project's constructor, but
350      * {@link Sources#getSourceGroups} may <em>not</em> be called within the
351      * constructor, as it requires the actual project object to exist and be
352      * registered in {@link ProjectManager}.
353      * </p>
354      * @return a source list object suitable for {@link Project#getLookup}
355      */

356     public Sources createSources() {
357         return new SourcesImpl();
358     }
359     
360     private final class SourcesImpl implements Sources, PropertyChangeListener JavaDoc, FileChangeSupportListener {
361         
362         private final List JavaDoc<ChangeListener JavaDoc> listeners = new ArrayList JavaDoc<ChangeListener JavaDoc>();
363         private boolean haveAttachedListeners;
364         private final Set JavaDoc<File JavaDoc> rootsListenedTo = new HashSet JavaDoc<File JavaDoc>();
365         /**
366          * The root URLs which were computed last, keyed by group type.
367          */

368         private final Map JavaDoc<String JavaDoc,List JavaDoc<URL JavaDoc>> lastComputedRoots = new HashMap JavaDoc<String JavaDoc,List JavaDoc<URL JavaDoc>>();
369         
370         public SourcesImpl() {
371             evaluator.addPropertyChangeListener(WeakListeners.propertyChange(this, evaluator));
372         }
373         
374         public SourceGroup[] getSourceGroups(String JavaDoc type) {
375             List JavaDoc<SourceGroup> groups = new ArrayList JavaDoc<SourceGroup>();
376             if (type.equals(Sources.TYPE_GENERIC)) {
377                 List JavaDoc<SourceRoot> roots = new ArrayList JavaDoc<SourceRoot>(principalSourceRoots);
378                 // Always include the project directory itself as a default:
379
roots.add(new SourceRoot("", ProjectUtils.getInformation(getProject()).getDisplayName(), null, null)); // NOI18N
380
Map JavaDoc<FileObject,SourceRoot> rootsByDir = new LinkedHashMap JavaDoc<FileObject,SourceRoot>();
381                 // First collect all non-redundant existing roots.
382
for (SourceRoot r : roots) {
383                     File JavaDoc locF = r.getActualLocation();
384                     if (locF == null) {
385                         continue;
386                     }
387                     listen(locF);
388                     FileObject loc = FileUtil.toFileObject(locF);
389                     if (loc == null) {
390                         continue;
391                     }
392                     if (rootsByDir.containsKey(loc)) {
393                         continue;
394                     }
395                     rootsByDir.put(loc, r);
396                 }
397                 // Remove subroots.
398
Iterator JavaDoc<FileObject> it = rootsByDir.keySet().iterator();
399                 while (it.hasNext()) {
400                     FileObject loc = it.next();
401                     FileObject parent = loc.getParent();
402                     while (parent != null) {
403                         if (rootsByDir.containsKey(parent)) {
404                             // This is a subroot of something, so skip it.
405
it.remove();
406                             break;
407                         }
408                         parent = parent.getParent();
409                     }
410                 }
411                 // Everything else is kosher.
412
for (Map.Entry JavaDoc<FileObject,SourceRoot> entry : rootsByDir.entrySet()) {
413                     groups.add(entry.getValue().toGroup(entry.getKey()));
414                 }
415             } else {
416                 Set JavaDoc<FileObject> dirs = new HashSet JavaDoc<FileObject>();
417                 for (TypedSourceRoot r : typedSourceRoots) {
418                     if (!r.getType().equals(type)) {
419                         continue;
420                     }
421                     File JavaDoc locF = r.getActualLocation();
422                     if (locF == null) {
423                         continue;
424                     }
425                     listen(locF);
426                     FileObject loc = FileUtil.toFileObject(locF);
427                     if (loc == null) {
428                         continue;
429                     }
430                     if (!dirs.add(loc)) {
431                         // Already had one.
432
continue;
433                     }
434                     groups.add(r.toGroup(loc));
435                 }
436             }
437             // Remember what we computed here so we know whether to fire changes later.
438
List JavaDoc<URL JavaDoc> rootURLs = new ArrayList JavaDoc<URL JavaDoc>(groups.size());
439             for (SourceGroup g : groups) {
440                 try {
441                     rootURLs.add(g.getRootFolder().getURL());
442                 } catch (FileStateInvalidException e) {
443                     assert false : e; // should be a valid file object!
444
}
445             }
446             lastComputedRoots.put(type, rootURLs);
447             return groups.toArray(new SourceGroup[groups.size()]);
448         }
449         
450         private void listen(File JavaDoc rootLocation) {
451             // #40845. Need to fire changes if a source root is added or removed.
452
if (rootsListenedTo.add(rootLocation) && /* be lazy */ haveAttachedListeners) {
453                 FileChangeSupport.DEFAULT.addListener(this, rootLocation);
454             }
455         }
456         
457         public void addChangeListener(ChangeListener JavaDoc listener) {
458             if (!haveAttachedListeners) {
459                 haveAttachedListeners = true;
460                 for (File JavaDoc rootLocation : rootsListenedTo) {
461                     FileChangeSupport.DEFAULT.addListener(this, rootLocation);
462                 }
463             }
464             synchronized (listeners) {
465                 listeners.add(listener);
466             }
467         }
468         
469         public void removeChangeListener(ChangeListener JavaDoc listener) {
470             synchronized (listeners) {
471                 listeners.remove(listener);
472             }
473         }
474         
475         private void fireChange() {
476             ChangeListener JavaDoc[] _listeners;
477             synchronized (listeners) {
478                 if (listeners.isEmpty()) {
479                     return;
480                 }
481                 _listeners = listeners.toArray(new ChangeListener JavaDoc[listeners.size()]);
482             }
483             ChangeEvent JavaDoc ev = new ChangeEvent JavaDoc(this);
484             for (ChangeListener JavaDoc l : _listeners) {
485                 l.stateChanged(ev);
486             }
487         }
488         
489         private void maybeFireChange() {
490             // #47451: check whether anything really changed.
491
boolean change = false;
492             // Cannot iterate over entrySet, as the map will be modified by getSourceGroups.
493
for (String JavaDoc type : new HashSet JavaDoc<String JavaDoc>(lastComputedRoots.keySet())) {
494                 List JavaDoc<URL JavaDoc> previous = new ArrayList JavaDoc<URL JavaDoc>(lastComputedRoots.get(type));
495                 getSourceGroups(type);
496                 List JavaDoc<URL JavaDoc> nue = lastComputedRoots.get(type);
497                 if (!nue.equals(previous)) {
498                     change = true;
499                     break;
500                 }
501             }
502             if (change) {
503                 fireChange();
504             }
505         }
506
507         public void fileCreated(FileChangeSupportEvent event) {
508             // Root might have been created on disk.
509
maybeFireChange();
510         }
511
512         public void fileDeleted(FileChangeSupportEvent event) {
513             // Root might have been deleted.
514
maybeFireChange();
515         }
516
517         public void fileModified(FileChangeSupportEvent event) {
518             // ignore; generally should not happen (listening to dirs)
519
}
520         
521         public void propertyChange(PropertyChangeEvent JavaDoc propertyChangeEvent) {
522             // Properties may have changed so as cause external roots to move etc.
523
maybeFireChange();
524         }
525
526     }
527     
528     private final class PropChangeL implements PropertyChangeListener JavaDoc {
529         
530         public PropChangeL() {}
531         
532         public void propertyChange(PropertyChangeEvent JavaDoc evt) {
533             // Some properties changed; external roots might have changed, so check them.
534
remarkExternalRoots();
535         }
536         
537     }
538     
539 }
540
Popular Tags