001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.spi.project.support.ant;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.beans.PropertyChangeSupport;
047: import java.io.File;
048: import java.io.IOException;
049: import java.net.URL;
050: import java.util.ArrayList;
051: import java.util.Collection;
052: import java.util.Collections;
053: import java.util.HashMap;
054: import java.util.HashSet;
055: import java.util.Iterator;
056: import java.util.LinkedHashMap;
057: import java.util.List;
058: import java.util.Map;
059: import java.util.Set;
060: import javax.swing.Icon;
061: import javax.swing.event.ChangeListener;
062: import org.netbeans.api.project.FileOwnerQuery;
063: import org.netbeans.api.project.Project;
064: import org.netbeans.api.project.ProjectManager;
065: import org.netbeans.api.project.ProjectUtils;
066: import org.netbeans.api.project.SourceGroup;
067: import org.netbeans.api.project.Sources;
068: import org.netbeans.api.queries.SharabilityQuery;
069: import org.netbeans.modules.project.ant.AntBasedProjectFactorySingleton;
070: import org.netbeans.modules.project.ant.FileChangeSupport;
071: import org.netbeans.modules.project.ant.FileChangeSupportEvent;
072: import org.netbeans.modules.project.ant.FileChangeSupportListener;
073: import org.openide.filesystems.FileObject;
074: import org.openide.filesystems.FileStateInvalidException;
075: import org.openide.filesystems.FileUtil;
076: import org.openide.util.ChangeSupport;
077: import org.openide.util.WeakListeners;
078:
079: // XXX should perhaps be legal to call add* methods at any time (should update things)
080: // and perhaps also have remove* methods
081: // and have code names for each source dir?
082:
083: // XXX should probably all be wrapped in ProjectManager.mutex
084:
085: /**
086: * Helper class to work with source roots and typed folders of a project.
087: * @author Jesse Glick
088: */
089: public final class SourcesHelper {
090:
091: private class Root {
092: protected final String location;
093:
094: public Root(String location) {
095: this .location = location;
096: }
097:
098: public final File getActualLocation() {
099: String val = evaluator.evaluate(location);
100: if (val == null) {
101: return null;
102: }
103: return project.resolveFile(val);
104: }
105:
106: public Collection<FileObject> getIncludeRoots() {
107: File loc = getActualLocation();
108: if (loc != null) {
109: FileObject fo = FileUtil.toFileObject(loc);
110: if (fo != null) {
111: return Collections.singleton(fo);
112: }
113: }
114: return Collections.emptySet();
115: }
116: }
117:
118: private class SourceRoot extends Root {
119:
120: private final String displayName;
121: private final Icon icon;
122: private final Icon openedIcon;
123: private final String includes;
124: private final String excludes;
125: private PathMatcher matcher;
126:
127: public SourceRoot(String location, String includes,
128: String excludes, String displayName, Icon icon,
129: Icon openedIcon) {
130: super (location);
131: this .displayName = displayName;
132: this .icon = icon;
133: this .openedIcon = openedIcon;
134: this .includes = includes;
135: this .excludes = excludes;
136: }
137:
138: public final SourceGroup toGroup(FileObject loc) {
139: assert loc != null;
140: return new Group(loc);
141: }
142:
143: @Override
144: public String toString() {
145: return "SourceRoot[" + location + "]"; // NOI18N
146: }
147:
148: // Copied w/ mods from GenericSources.
149: private final class Group implements SourceGroup,
150: PropertyChangeListener {
151:
152: private final FileObject loc;
153: private final PropertyChangeSupport pcs = new PropertyChangeSupport(
154: this );
155:
156: Group(FileObject loc) {
157: this .loc = loc;
158: evaluator.addPropertyChangeListener(WeakListeners
159: .propertyChange(this , evaluator));
160: }
161:
162: public FileObject getRootFolder() {
163: return loc;
164: }
165:
166: public String getName() {
167: return location.length() > 0 ? location : "generic"; // NOI18N
168: }
169:
170: public String getDisplayName() {
171: return displayName;
172: }
173:
174: public Icon getIcon(boolean opened) {
175: return opened ? icon : openedIcon;
176: }
177:
178: public boolean contains(FileObject file)
179: throws IllegalArgumentException {
180: if (file == loc) {
181: return true;
182: }
183: String path = FileUtil.getRelativePath(loc, file);
184: if (path == null) {
185: throw new IllegalArgumentException(file
186: + " is not inside " + loc);
187: }
188: if (file.isFolder()) {
189: path += "/"; // NOI18N
190: }
191: computeIncludeExcludePatterns();
192: if (!matcher.matches(path, true)) {
193: return false;
194: }
195: Project p = getProject();
196: if (file.isFolder() && file != p.getProjectDirectory()
197: && ProjectManager.getDefault().isProject(file)) {
198: // #67450: avoid actually loading the nested project.
199: return false;
200: }
201: if (!(SourceRoot.this instanceof TypedSourceRoot)) {
202: // XXX disabled for typed source roots; difficult to make fast (#97215)
203: Project owner = FileOwnerQuery.getOwner(file);
204: if (owner != null && owner != p) {
205: return false;
206: }
207: File f = FileUtil.toFile(file);
208: if (f != null
209: && SharabilityQuery.getSharability(f) == SharabilityQuery.NOT_SHARABLE) {
210: return false;
211: } // else MIXED, UNKNOWN, or SHARABLE; or not a disk file
212: }
213: return true;
214: }
215:
216: public void addPropertyChangeListener(
217: PropertyChangeListener l) {
218: pcs.addPropertyChangeListener(l);
219: }
220:
221: public void removePropertyChangeListener(
222: PropertyChangeListener l) {
223: pcs.removePropertyChangeListener(l);
224: }
225:
226: @Override
227: public String toString() {
228: return "SourcesHelper.Group[name=" + getName()
229: + ",rootFolder=" + getRootFolder() + "]"; // NOI18N
230: }
231:
232: public void propertyChange(PropertyChangeEvent ev) {
233: assert ev.getSource() == evaluator : ev;
234: String prop = ev.getPropertyName();
235: if (prop == null
236: || (includes != null && includes.contains("${"
237: + prop + "}")) || // NOI18N
238: (excludes != null && excludes.contains("${"
239: + prop + "}"))) { // NOI18N
240: matcher = null;
241: pcs.firePropertyChange(PROP_CONTAINERSHIP, null,
242: null);
243: }
244: // XXX should perhaps react to ProjectInformation changes? but nothing to fire currently
245: }
246:
247: }
248:
249: private String evalForMatcher(String raw) {
250: if (raw == null) {
251: return null;
252: }
253: String patterns = evaluator.evaluate(raw);
254: if (patterns == null) {
255: return null;
256: }
257: if (patterns.matches("\\$\\{[^}]+\\}")) { // NOI18N
258: // Unevaluated single property, treat like null.
259: return null;
260: }
261: return patterns;
262: }
263:
264: private void computeIncludeExcludePatterns() {
265: if (matcher != null) {
266: return;
267: }
268: String includesPattern = evalForMatcher(includes);
269: String excludesPattern = evalForMatcher(excludes);
270: matcher = new PathMatcher(includesPattern, excludesPattern,
271: getActualLocation());
272: }
273:
274: @Override
275: public Collection<FileObject> getIncludeRoots() {
276: Collection<FileObject> supe = super .getIncludeRoots();
277: computeIncludeExcludePatterns();
278: if (supe.size() == 1) {
279: Set<FileObject> roots = new HashSet<FileObject>();
280: for (File r : matcher.findIncludedRoots()) {
281: FileObject subroot = FileUtil.toFileObject(r);
282: if (subroot != null) {
283: roots.add(subroot);
284: }
285: }
286: return roots;
287: } else {
288: assert supe.isEmpty();
289: return supe;
290: }
291: }
292:
293: }
294:
295: private final class TypedSourceRoot extends SourceRoot {
296: private final String type;
297:
298: public TypedSourceRoot(String type, String location,
299: String includes, String excludes, String displayName,
300: Icon icon, Icon openedIcon) {
301: super (location, includes, excludes, displayName, icon,
302: openedIcon);
303: this .type = type;
304: }
305:
306: public final String getType() {
307: return type;
308: }
309: }
310:
311: private final AntProjectHelper project;
312: private final PropertyEvaluator evaluator;
313: private final List<SourceRoot> principalSourceRoots = new ArrayList<SourceRoot>();
314: private final List<Root> nonSourceRoots = new ArrayList<Root>();
315: private final List<Root> ownedFiles = new ArrayList<Root>();
316: private final List<TypedSourceRoot> typedSourceRoots = new ArrayList<TypedSourceRoot>();
317: private int registeredRootAlgorithm;
318: /**
319: * If not null, external roots that we registered the last time.
320: * Used when a property change is encountered, to see if the set of external
321: * roots might have changed. Hold the actual files (not e.g. URLs); see
322: * {@link #registerExternalRoots} for the reason why.
323: */
324: private Set<FileObject> lastRegisteredRoots;
325: private PropertyChangeListener propChangeL;
326:
327: /**
328: * Create the helper object, initially configured to recognize only sources
329: * contained inside the project directory.
330: * @param project an Ant project helper
331: * @param evaluator a way to evaluate Ant properties used to define source locations
332: */
333: public SourcesHelper(AntProjectHelper project,
334: PropertyEvaluator evaluator) {
335: this .project = project;
336: this .evaluator = evaluator;
337: }
338:
339: /**
340: * Add a possible principal source root, or top-level folder which may
341: * contain sources that should be considered part of the project.
342: * <p>
343: * If the actual value of the location is inside the project directory,
344: * this is simply ignored; so it safe to configure principal source roots
345: * for any source directory which might be set to use an external path, even
346: * if the common location is internal.
347: * </p>
348: * @param location a project-relative or absolute path giving the location
349: * of a source tree; may contain Ant property substitutions
350: * @param displayName a display name (for {@link SourceGroup#getDisplayName})
351: * @param icon a regular icon for the source root, or null
352: * @param openedIcon an opened variant icon for the source root, or null
353: * @throws IllegalStateException if this method is called after either
354: * {@link #createSources} or {@link #registerExternalRoots}
355: * was called
356: * @see #registerExternalRoots
357: * @see Sources#TYPE_GENERIC
358: */
359: public void addPrincipalSourceRoot(String location,
360: String displayName, Icon icon, Icon openedIcon)
361: throws IllegalStateException {
362: addPrincipalSourceRoot(location, null, null, displayName, icon,
363: openedIcon);
364: }
365:
366: /**
367: * Add a possible principal source root, or top-level folder which may
368: * contain sources that should be considered part of the project, with
369: * optional include and exclude lists.
370: * <p>
371: * If an include or exclude string is given as null, then it is skipped. A non-null value is
372: * evaluated and then treated as a comma- or space-separated pattern list,
373: * as detailed in the Javadoc for {@link PathMatcher}.
374: * (As a special convenience, a value consisting solely of an Ant property reference
375: * which cannot be evaluated, e.g. <samp>${undefined}</samp>, is treated like null.)
376: * {@link SourceGroup#contains} will then reflect the includes and excludes for files, but note that the
377: * semantics of that method requires that a folder be "contained" in case any folder or file
378: * beneath it is contained, and in particular the root folder is always contained.
379: * </p>
380: * @param location a project-relative or absolute path giving the location
381: * of a source tree; may contain Ant property substitutions
382: * @param includes Ant-style includes; may contain Ant property substitutions;
383: * if not null, only files and folders
384: * matching the pattern (or patterns), and not specified in the excludes list,
385: * will be {@link SourceGroup#contains included}
386: * @param excludes Ant-style excludes; may contain Ant property substitutions;
387: * if not null, files and folders
388: * matching the pattern (or patterns) will not be {@link SourceGroup#contains included},
389: * even if specified in the includes list
390: * @param displayName a display name (for {@link SourceGroup#getDisplayName})
391: * @param icon a regular icon for the source root, or null
392: * @param openedIcon an opened variant icon for the source root, or null
393: * @throws IllegalStateException if this method is called after either
394: * {@link #createSources} or {@link #registerExternalRoots}
395: * was called
396: * @see #registerExternalRoots
397: * @see Sources#TYPE_GENERIC
398: * @since org.netbeans.modules.project.ant/1 1.15
399: */
400: public void addPrincipalSourceRoot(String location,
401: String includes, String excludes, String displayName,
402: Icon icon, Icon openedIcon) throws IllegalStateException {
403: if (lastRegisteredRoots != null) {
404: throw new IllegalStateException(
405: "registerExternalRoots was already called"); // NOI18N
406: }
407: principalSourceRoots.add(new SourceRoot(location, includes,
408: excludes, displayName, icon, openedIcon));
409: }
410:
411: /**
412: * Similar to {@link #addPrincipalSourceRoot} but affects only
413: * {@link #registerExternalRoots} and not {@link #createSources}.
414: * <p class="nonnormative">
415: * Useful for project type providers which have external paths holding build
416: * products. These should not appear in {@link Sources}, yet it may be useful
417: * for {@link FileOwnerQuery} to know the owning project (for example, in order
418: * for a project-specific <a href="@org-netbeans-api-java@/org/netbeans/spi/java/queries/SourceForBinaryQueryImplementation.html"><code>SourceForBinaryQueryImplementation</code></a> to work).
419: * </p>
420: * @param location a project-relative or absolute path giving the location
421: * of a non-source tree; may contain Ant property substitutions
422: * @throws IllegalStateException if this method is called after
423: * {@link #registerExternalRoots} was called
424: */
425: public void addNonSourceRoot(String location)
426: throws IllegalStateException {
427: if (lastRegisteredRoots != null) {
428: throw new IllegalStateException(
429: "registerExternalRoots was already called"); // NOI18N
430: }
431: nonSourceRoots.add(new Root(location));
432: }
433:
434: /**
435: * Add any file that is supposed to be owned by a given project
436: * via FileOwnerQuery, affects only {@link #registerExternalRoots}
437: * and not {@link #createSources}.
438: * <p class="nonnormative">
439: * Useful for project type providers which have external paths holding build
440: * products. These should not appear in {@link Sources}, yet it may be useful
441: * for {@link FileOwnerQuery} to know the owning project (for example, in order
442: * for a project-specific <a href="@org-netbeans-api-java@/org/netbeans/spi/java/queries/SourceForBinaryQueryImplementation.html"><code>SourceForBinaryQueryImplementation</code></a> to work).
443: * </p>
444: * @param location a project-relative or absolute path giving the location
445: * of a file; may contain Ant property substitutions
446: * @throws IllegalStateException if this method is called after
447: * {@link #registerExternalRoots} was called
448: * @since org.netbeans.modules.project.ant/1 1.17
449: */
450: public void addOwnedFile(String location)
451: throws IllegalStateException {
452: if (lastRegisteredRoots != null) {
453: throw new IllegalStateException(
454: "registerExternalRoots was already called"); // NOI18N
455: }
456: ownedFiles.add(new Root(location));
457: }
458:
459: /**
460: * Add a typed source root which will be considered only in certain contexts.
461: * @param location a project-relative or absolute path giving the location
462: * of a source tree; may contain Ant property substitutions
463: * @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>
464: * @param displayName a display name (for {@link SourceGroup#getDisplayName})
465: * @param icon a regular icon for the source root, or null
466: * @param openedIcon an opened variant icon for the source root, or null
467: * @throws IllegalStateException if this method is called after either
468: * {@link #createSources} or {@link #registerExternalRoots}
469: * was called
470: */
471: public void addTypedSourceRoot(String location, String type,
472: String displayName, Icon icon, Icon openedIcon)
473: throws IllegalStateException {
474: addTypedSourceRoot(location, null, null, type, displayName,
475: icon, openedIcon);
476: }
477:
478: /**
479: * Add a typed source root with optional include and exclude lists.
480: * See {@link #addPrincipalSourceRoot(String,String,String,String,Icon,Icon)}
481: * for details on semantics of includes and excludes.
482: * @param location a project-relative or absolute path giving the location
483: * of a source tree; may contain Ant property substitutions
484: * @param includes an optional list of Ant-style includes
485: * @param excludes an optional list of Ant-style excludes
486: * @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>
487: * @param displayName a display name (for {@link SourceGroup#getDisplayName})
488: * @param icon a regular icon for the source root, or null
489: * @param openedIcon an opened variant icon for the source root, or null
490: * @throws IllegalStateException if this method is called after either
491: * {@link #createSources} or {@link #registerExternalRoots}
492: * was called
493: * @since org.netbeans.modules.project.ant/1 1.15
494: */
495: public void addTypedSourceRoot(String location, String includes,
496: String excludes, String type, String displayName,
497: Icon icon, Icon openedIcon) throws IllegalStateException {
498: if (lastRegisteredRoots != null) {
499: throw new IllegalStateException(
500: "registerExternalRoots was already called"); // NOI18N
501: }
502: typedSourceRoots.add(new TypedSourceRoot(type, location,
503: includes, excludes, displayName, icon, openedIcon));
504: }
505:
506: private Project getProject() {
507: return AntBasedProjectFactorySingleton.getProjectFor(project);
508: }
509:
510: /**
511: * Register all external source or non-source roots using {@link FileOwnerQuery#markExternalOwner}.
512: * <p>
513: * Only roots added by {@link #addPrincipalSourceRoot} and {@link #addNonSourceRoot}
514: * are considered. They are registered if (and only if) they in fact fall
515: * outside of the project directory, and of course only if the folders really
516: * exist on disk. Currently it is not defined when this file existence check
517: * is done (e.g. when this method is first called, or periodically) or whether
518: * folders which are created subsequently will be registered, so project type
519: * providers are encouraged to create all desired external roots before calling
520: * this method.
521: * </p>
522: * <p>
523: * If the actual value of the location changes (due to changes being
524: * fired from the property evaluator), roots which were previously internal
525: * and are now external will be registered, and roots which were previously
526: * external and are now internal will be unregistered. The (un-)registration
527: * will be done using the same algorithm as was used initially.
528: * </p>
529: * <p>
530: * If an explicit include list is configured for a principal source root, only those
531: * subfolders which are included (or folders directly containing included files)
532: * will be registered. Note that the source root, or an included subfolder, will
533: * be registered even if it contains excluded files or folders beneath it.
534: * </p>
535: * <p>
536: * Calling this method causes the helper object to hold strong references to the
537: * current external roots, which helps a project satisfy the requirements of
538: * {@link FileOwnerQuery#EXTERNAL_ALGORITHM_TRANSIENT}.
539: * </p>
540: * <p>
541: * You may <em>not</em> call this method inside the project's constructor, as
542: * it requires the actual project to exist and be registered in {@link ProjectManager}.
543: * Typically you would use {@link org.openide.util.Mutex#postWriteRequest} to run it
544: * later, if you were creating the helper in your constructor, since the project construction
545: * normally occurs in read access.
546: * </p>
547: * @param algorithm an external root registration algorithm as per
548: * {@link FileOwnerQuery#markExternalOwner}
549: * @throws IllegalArgumentException if the algorithm is unrecognized
550: * @throws IllegalStateException if this method is called more than once on a
551: * given <code>SourcesHelper</code> object
552: */
553: public void registerExternalRoots(int algorithm)
554: throws IllegalArgumentException, IllegalStateException {
555: if (lastRegisteredRoots != null) {
556: throw new IllegalStateException(
557: "registerExternalRoots was already called before"); // NOI18N
558: }
559: registeredRootAlgorithm = algorithm;
560: remarkExternalRoots();
561: }
562:
563: private void remarkExternalRoots() throws IllegalArgumentException {
564: List<Root> allRoots = new ArrayList<Root>(principalSourceRoots);
565: allRoots.addAll(nonSourceRoots);
566: allRoots.addAll(ownedFiles);
567: Project p = getProject();
568: FileObject pdir = project.getProjectDirectory();
569: // First time: register roots and add to lastRegisteredRoots.
570: // Subsequent times: add to newRootsToRegister and maybe add them later.
571: if (lastRegisteredRoots == null) {
572: // First time.
573: lastRegisteredRoots = Collections.emptySet();
574: propChangeL = new PropChangeL(); // hold a strong ref
575: evaluator.addPropertyChangeListener(WeakListeners
576: .propertyChange(propChangeL, evaluator));
577: }
578: Set<FileObject> newRegisteredRoots = new HashSet<FileObject>();
579: // XXX might be a bit more efficient to cache for each root the actualLocation value
580: // that was last computed, and just check if that has changed... otherwise we wind
581: // up calling APH.resolveFileObject repeatedly (for each property change)
582: for (Root r : allRoots) {
583: for (FileObject loc : r.getIncludeRoots()) {
584: if (FileUtil.getRelativePath(pdir, loc) != null) {
585: // Inside projdir already. Skip it.
586: continue;
587: }
588: if (loc.isFolder()) {
589: try {
590: Project other = ProjectManager.getDefault()
591: .findProject(loc);
592: if (other != null) {
593: // This is a foreign project; we cannot own it. Skip it.
594: continue;
595: }
596: } catch (IOException e) {
597: // Assume it is a foreign project and skip it.
598: continue;
599: }
600: }
601: // It's OK to go.
602: newRegisteredRoots.add(loc);
603: }
604: }
605: // Just check for changes since the last time.
606: Set<FileObject> toUnregister = new HashSet<FileObject>(
607: lastRegisteredRoots);
608: toUnregister.removeAll(newRegisteredRoots);
609: for (FileObject loc : toUnregister) {
610: FileOwnerQuery.markExternalOwner(loc, null,
611: registeredRootAlgorithm);
612: }
613: Set<FileObject> toRegister = new HashSet<FileObject>(
614: newRegisteredRoots);
615: toRegister.removeAll(lastRegisteredRoots);
616: for (FileObject loc : toRegister) {
617: FileOwnerQuery.markExternalOwner(loc, p,
618: registeredRootAlgorithm);
619: }
620: lastRegisteredRoots = newRegisteredRoots;
621: }
622:
623: /**
624: * Create a source list object.
625: * <p>
626: * All principal source roots are listed as {@link Sources#TYPE_GENERIC} unless they
627: * are inside the project directory. The project directory itself is also listed
628: * (with a display name according to {@link ProjectUtils#getInformation}), unless
629: * it is contained by an explicit principal source root (i.e. ancestor directory).
630: * Principal source roots should never overlap; if two configured
631: * principal source roots are determined to have the same root folder, the first
632: * configured root takes precedence (which only matters in regard to the display
633: * name); if one root folder is contained within another, the broader
634: * root folder subsumes the narrower one so only the broader root is listed.
635: * </p>
636: * <p>
637: * Other source groups are listed according to the named typed source roots.
638: * There is no check performed that these do not overlap (though a project type
639: * provider should for UI reasons avoid this situation).
640: * </p>
641: * <p>
642: * Any source roots which do not exist on disk are ignored, as if they had
643: * not been configured at all. Currently it is not defined when this existence
644: * check is performed (e.g. when this method is called, when the source root
645: * is first accessed, periodically, etc.), so project type providers are
646: * generally encouraged to make sure all desired source folders exist
647: * before calling this method, if creating a new project.
648: * </p>
649: * <p>
650: * Source groups are created according to the semantics described in
651: * {@link org.netbeans.spi.project.support.GenericSources#group}. They are listed in the order they
652: * were configured (for those roots that are actually used as groups).
653: * </p>
654: * <p>
655: * You may call this method inside the project's constructor, but
656: * {@link Sources#getSourceGroups} may <em>not</em> be called within the
657: * constructor, as it requires the actual project object to exist and be
658: * registered in {@link ProjectManager}.
659: * </p>
660: * @return a source list object suitable for {@link Project#getLookup}
661: */
662: public Sources createSources() {
663: return new SourcesImpl();
664: }
665:
666: private final class SourcesImpl implements Sources,
667: PropertyChangeListener, FileChangeSupportListener {
668:
669: private final ChangeSupport cs = new ChangeSupport(this );
670: private boolean haveAttachedListeners;
671: private final Set<File> rootsListenedTo = new HashSet<File>();
672: /**
673: * The root URLs which were computed last, keyed by group type.
674: */
675: private final Map<String, List<URL>> lastComputedRoots = new HashMap<String, List<URL>>();
676:
677: public SourcesImpl() {
678: evaluator.addPropertyChangeListener(WeakListeners
679: .propertyChange(this , evaluator));
680: }
681:
682: public SourceGroup[] getSourceGroups(String type) {
683: List<SourceGroup> groups = new ArrayList<SourceGroup>();
684: if (type.equals(Sources.TYPE_GENERIC)) {
685: List<SourceRoot> roots = new ArrayList<SourceRoot>(
686: principalSourceRoots);
687: // Always include the project directory itself as a default:
688: roots.add(new SourceRoot("", null, null, ProjectUtils
689: .getInformation(getProject()).getDisplayName(),
690: null, null)); // NOI18N
691: Map<FileObject, SourceRoot> rootsByDir = new LinkedHashMap<FileObject, SourceRoot>();
692: // First collect all non-redundant existing roots.
693: for (SourceRoot r : roots) {
694: File locF = r.getActualLocation();
695: if (locF == null) {
696: continue;
697: }
698: listen(locF);
699: FileObject loc = FileUtil.toFileObject(locF);
700: if (loc == null) {
701: continue;
702: }
703: if (rootsByDir.containsKey(loc)) {
704: continue;
705: }
706: rootsByDir.put(loc, r);
707: }
708: // Remove subroots.
709: Iterator<FileObject> it = rootsByDir.keySet()
710: .iterator();
711: while (it.hasNext()) {
712: FileObject loc = it.next();
713: FileObject parent = loc.getParent();
714: while (parent != null) {
715: if (rootsByDir.containsKey(parent)) {
716: // This is a subroot of something, so skip it.
717: it.remove();
718: break;
719: }
720: parent = parent.getParent();
721: }
722: }
723: // Everything else is kosher.
724: for (Map.Entry<FileObject, SourceRoot> entry : rootsByDir
725: .entrySet()) {
726: groups
727: .add(entry.getValue().toGroup(
728: entry.getKey()));
729: }
730: } else {
731: Set<FileObject> dirs = new HashSet<FileObject>();
732: for (TypedSourceRoot r : typedSourceRoots) {
733: if (!r.getType().equals(type)) {
734: continue;
735: }
736: File locF = r.getActualLocation();
737: if (locF == null) {
738: continue;
739: }
740: listen(locF);
741: FileObject loc = FileUtil.toFileObject(locF);
742: if (loc == null) {
743: continue;
744: }
745: if (!dirs.add(loc)) {
746: // Already had one.
747: continue;
748: }
749: groups.add(r.toGroup(loc));
750: }
751: }
752: // Remember what we computed here so we know whether to fire changes later.
753: List<URL> rootURLs = new ArrayList<URL>(groups.size());
754: for (SourceGroup g : groups) {
755: try {
756: rootURLs.add(g.getRootFolder().getURL());
757: } catch (FileStateInvalidException e) {
758: assert false : e; // should be a valid file object!
759: }
760: }
761: lastComputedRoots.put(type, rootURLs);
762: return groups.toArray(new SourceGroup[groups.size()]);
763: }
764:
765: private synchronized void listen(File rootLocation) {
766: // #40845. Need to fire changes if a source root is added or removed.
767: if (rootsListenedTo.add(rootLocation)
768: && /* be lazy */haveAttachedListeners) {
769: FileChangeSupport.DEFAULT.addListener(this ,
770: rootLocation);
771: }
772: }
773:
774: public synchronized void addChangeListener(
775: ChangeListener listener) {
776: if (!haveAttachedListeners) {
777: haveAttachedListeners = true;
778: for (File rootLocation : rootsListenedTo) {
779: FileChangeSupport.DEFAULT.addListener(this ,
780: rootLocation);
781: }
782: }
783: cs.addChangeListener(listener);
784: }
785:
786: public void removeChangeListener(ChangeListener listener) {
787: cs.removeChangeListener(listener);
788: }
789:
790: private void maybeFireChange() {
791: // #47451: check whether anything really changed.
792: boolean change = false;
793: // Cannot iterate over entrySet, as the map will be modified by getSourceGroups.
794: for (String type : new HashSet<String>(lastComputedRoots
795: .keySet())) {
796: List<URL> previous = new ArrayList<URL>(
797: lastComputedRoots.get(type));
798: getSourceGroups(type);
799: List<URL> nue = lastComputedRoots.get(type);
800: if (!nue.equals(previous)) {
801: change = true;
802: break;
803: }
804: }
805: if (change) {
806: cs.fireChange();
807: }
808: }
809:
810: public void fileCreated(FileChangeSupportEvent event) {
811: // Root might have been created on disk.
812: maybeFireChange();
813: }
814:
815: public void fileDeleted(FileChangeSupportEvent event) {
816: // Root might have been deleted.
817: maybeFireChange();
818: }
819:
820: public void fileModified(FileChangeSupportEvent event) {
821: // ignore; generally should not happen (listening to dirs)
822: }
823:
824: public void propertyChange(
825: PropertyChangeEvent propertyChangeEvent) {
826: // Properties may have changed so as cause external roots to move etc.
827: maybeFireChange();
828: }
829:
830: }
831:
832: private final class PropChangeL implements PropertyChangeListener {
833:
834: public PropChangeL() {
835: }
836:
837: public void propertyChange(PropertyChangeEvent evt) {
838: // Some properties changed; external roots might have changed, so check them.
839: for (SourceRoot r : principalSourceRoots) {
840: r.matcher = null;
841: }
842: remarkExternalRoots();
843: }
844:
845: }
846:
847: }
|