/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby.spi.project.support.rake;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.Icon;
import javax.swing.event.ChangeListener;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.spi.project.support.GenericSources;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.util.ChangeSupport;
import org.openide.util.WeakListeners;
// XXX should perhaps be legal to call add* methods at any time (should update things)
// and perhaps also have remove* methods
// and have code names for each source dir?
// XXX should probably all be wrapped in ProjectManager.mutex
/**
* Helper class to work with source roots and typed folders of a project.
* @author Jesse Glick
*/
public final class SourcesHelper {
private class Root {
protected final String location;
public Root(String location) {
this.location = location;
}
public final File getActualLocation() {
String val = evaluator.evaluate(location);
if (val == null) {
return null;
}
return project.resolveFile(val);
}
}
private class SourceRoot extends Root {
private final String displayName;
private final Icon icon;
private final Icon openedIcon;
public SourceRoot(String location, String displayName, Icon icon, Icon openedIcon) {
super(location);
this.displayName = displayName;
this.icon = icon;
this.openedIcon = openedIcon;
}
public final SourceGroup toGroup(FileObject loc) {
assert loc != null;
return GenericSources.group(getProject(), loc, location.length() > 0 ? location : "generic", // NOI18N
displayName, icon, openedIcon);
}
}
private final class TypedSourceRoot extends SourceRoot {
private final String type;
public TypedSourceRoot(String type, String location, String displayName, Icon icon, Icon openedIcon) {
super(location, displayName, icon, openedIcon);
this.type = type;
}
public final String getType() {
return type;
}
}
private final Project prj;
private final RakeProjectHelper project;
private final PropertyEvaluator evaluator;
private final List<SourceRoot> principalSourceRoots = new ArrayList<SourceRoot>();
private final List<Root> nonSourceRoots = new ArrayList<Root>();
private final List<TypedSourceRoot> typedSourceRoots = new ArrayList<TypedSourceRoot>();
private int registeredRootAlgorithm;
/**
* If not null, external roots that we registered the last time.
* Used when a property change is encountered, to see if the set of external
* roots might have changed. Hold the actual files (not e.g. URLs); see
* {@link #registerExternalRoots} for the reason why.
*/
private Set<FileObject> lastRegisteredRoots;
private PropertyChangeListener propChangeL;
/**
* Create the helper object, initially configured to recognize only sources
* contained inside the project directory.
* @param project an Ant project helper
* @param evaluator a way to evaluate Ant properties used to define source locations
*/
public SourcesHelper(Project prj, RakeProjectHelper project, PropertyEvaluator evaluator) {
this.prj = prj;
this.project = project;
this.evaluator = evaluator;
}
/**
* Add a possible principal source root, or top-level folder which may
* contain sources that should be considered part of the project.
* <p>
* If the actual value of the location is inside the project directory,
* this is simply ignored; so it safe to configure principal source roots
* for any source directory which might be set to use an external path, even
* if the common location is internal.
* </p>
* @param location a project-relative or absolute path giving the location
* of a source tree; may contain Ant property substitutions
* @param displayName a display name (for {@link SourceGroup#getDisplayName})
* @param icon a regular icon for the source root, or null
* @param openedIcon an opened variant icon for the source root, or null
* @throws IllegalStateException if this method is called after either
* {@link #createSources} or {@link #registerExternalRoots}
* was called
* @see #registerExternalRoots
* @see Sources#TYPE_GENERIC
*/
public void addPrincipalSourceRoot(String location, String displayName, Icon icon, Icon openedIcon) throws IllegalStateException {
if (lastRegisteredRoots != null) {
throw new IllegalStateException("registerExternalRoots was already called"); // NOI18N
}
principalSourceRoots.add(new SourceRoot(location, displayName, icon, openedIcon));
}
/**
* Similar to {@link #addPrincipalSourceRoot} but affects only
* {@link #registerExternalRoots} and not {@link #createSources}.
* <p class="nonnormative">
* Useful for project type providers which have external paths holding build
* products. These should not appear in {@link Sources}, yet it may be useful
* for {@link FileOwnerQuery} to know the owning project (for example, in order
* for a project-specific {@link org.netbeans.spi.queries.SourceForBinaryQueryImplementation} to work).
* </p>
* @param location a project-relative or absolute path giving the location
* of a non-source tree; may contain Ant property substitutions
* @throws IllegalStateException if this method is called after
* {@link #registerExternalRoots} was called
*/
public void addNonSourceRoot(String location) throws IllegalStateException {
if (lastRegisteredRoots != null) {
throw new IllegalStateException("registerExternalRoots was already called"); // NOI18N
}
nonSourceRoots.add(new Root(location));
}
/**
* Add a typed source root which will be considered only in certain contexts.
* @param location a project-relative or absolute path giving the location
* of a source tree; may contain Ant property substitutions
* @param type a source root type such as <a href="@org-netbeans-modules-java-project@/org/netbeans/modules/gsfpath/api/project/JavaProjectConstants.html#SOURCES_TYPE_JAVA"><code>JavaProjectConstants.SOURCES_TYPE_JAVA</code></a>
* @param displayName a display name (for {@link SourceGroup#getDisplayName})
* @param icon a regular icon for the source root, or null
* @param openedIcon an opened variant icon for the source root, or null
* @throws IllegalStateException if this method is called after either
* {@link #createSources} or {@link #registerExternalRoots}
* was called
*/
public void addTypedSourceRoot(String location, String type, String displayName, Icon icon, Icon openedIcon) throws IllegalStateException {
if (lastRegisteredRoots != null) {
throw new IllegalStateException("registerExternalRoots was already called"); // NOI18N
}
typedSourceRoots.add(new TypedSourceRoot(type, location, displayName, icon, openedIcon));
}
private Project getProject() {
return prj;
}
/**
* Register all external source or non-source roots using {@link FileOwnerQuery#markExternalOwner}.
* <p>
* Only roots added by {@link #addPrincipalSourceRoot} and {@link #addNonSourceRoot}
* are considered. They are registered if (and only if) they in fact fall
* outside of the project directory, and of course only if the folders really
* exist on disk. Currently it is not defined when this file existence check
* is done (e.g. when this method is first called, or periodically) or whether
* folders which are created subsequently will be registered, so project type
* providers are encouraged to create all desired external roots before calling
* this method.
* </p>
* <p>
* If the actual value of the location changes (due to changes being
* fired from the property evaluator), roots which were previously internal
* and are now external will be registered, and roots which were previously
* external and are now internal will be unregistered. The (un-)registration
* will be done using the same algorithm as was used initially.
* </p>
* <p>
* Calling this method causes the helper object to hold strong references to the
* current external roots, which helps a project satisfy the requirements of
* {@link FileOwnerQuery#EXTERNAL_ALGORITHM_TRANSIENT}.
* </p>
* @param algorithm an external root registration algorithm as per
* {@link FileOwnerQuery#markExternalOwner}
* @throws IllegalArgumentException if the algorithm is unrecognized
* @throws IllegalStateException if this method is called more than once on a
* given <code>SourcesHelper</code> object
*/
public void registerExternalRoots(int algorithm) throws IllegalArgumentException, IllegalStateException {
if (lastRegisteredRoots != null) {
throw new IllegalStateException("registerExternalRoots was already called before"); // NOI18N
}
registeredRootAlgorithm = algorithm;
remarkExternalRoots();
}
private void remarkExternalRoots() throws IllegalArgumentException {
List<Root> allRoots = new ArrayList<Root>(principalSourceRoots);
allRoots.addAll(nonSourceRoots);
Project p = getProject();
FileObject pdir = project.getProjectDirectory();
// First time: register roots and add to lastRegisteredRoots.
// Subsequent times: add to newRootsToRegister and maybe add them later.
Set<FileObject> newRootsToRegister;
if (lastRegisteredRoots == null) {
// First time.
newRootsToRegister = null;
lastRegisteredRoots = new HashSet<FileObject>();
propChangeL = new PropChangeL(); // hold a strong ref
evaluator.addPropertyChangeListener(WeakListeners.propertyChange(propChangeL, evaluator));
} else {
newRootsToRegister = new HashSet<FileObject>();
}
// XXX might be a bit more efficient to cache for each root the actualLocation value
// that was last computed, and just check if that has changed... otherwise we wind
// up calling APH.resolveFileObject repeatedly (for each property change)
for (Root r : allRoots) {
File locF = r.getActualLocation();
FileObject loc = locF != null ? FileUtil.toFileObject(locF) : null;
if (loc == null) {
// Not there; skip it.
continue;
}
if (!loc.isFolder()) {
// Actually a file. Skip it.
continue;
}
if (FileUtil.getRelativePath(pdir, loc) != null) {
// Inside projdir already. Skip it.
continue;
}
try {
Project other = ProjectManager.getDefault().findProject(loc);
if (other != null) {
// This is a foreign project; we cannot own it. Skip it.
continue;
}
} catch (IOException e) {
// Assume it is a foreign project and skip it.
continue;
}
// It's OK to go.
if (newRootsToRegister != null) {
newRootsToRegister.add(loc);
} else {
lastRegisteredRoots.add(loc);
FileOwnerQuery.markExternalOwner(loc, p, registeredRootAlgorithm);
}
}
if (newRootsToRegister != null) {
// Just check for changes since the last time.
Set<FileObject> toUnregister = new HashSet<FileObject>(lastRegisteredRoots);
toUnregister.removeAll(newRootsToRegister);
for (FileObject loc : toUnregister) {
FileOwnerQuery.markExternalOwner(loc, null, registeredRootAlgorithm);
}
newRootsToRegister.removeAll(lastRegisteredRoots);
for (FileObject loc : newRootsToRegister) {
FileOwnerQuery.markExternalOwner(loc, p, registeredRootAlgorithm);
}
}
}
/**
* Create a source list object.
* <p>
* All principal source roots are listed as {@link Sources#TYPE_GENERIC} unless they
* are inside the project directory. The project directory itself is also listed
* (with a display name according to {@link ProjectUtils#getInformation}), unless
* it is contained by an explicit principal source root (i.e. ancestor directory).
* Principal source roots should never overlap; if two configured
* principal source roots are determined to have the same root folder, the first
* configured root takes precedence (which only matters in regard to the display
* name); if one root folder is contained within another, the broader
* root folder subsumes the narrower one so only the broader root is listed.
* </p>
* <p>
* Other source groups are listed according to the named typed source roots.
* There is no check performed that these do not overlap (though a project type
* provider should for UI reasons avoid this situation).
* </p>
* <p>
* Any source roots which do not exist on disk are ignored, as if they had
* not been configured at all. Currently it is not defined when this existence
* check is performed (e.g. when this method is called, when the source root
* is first accessed, periodically, etc.), so project type providers are
* generally encouraged to make sure all desired source folders exist
* before calling this method, if creating a new project.
* </p>
* <p>
* Source groups are created according to the semantics described in
* {@link GenericSources#group}. They are listed in the order they
* were configured (for those roots that are actually used as groups).
* </p>
* <p>
* You may call this method inside the project's constructor, but
* {@link Sources#getSourceGroups} may <em>not</em> be called within the
* constructor, as it requires the actual project object to exist and be
* registered in {@link ProjectManager}.
* </p>
* @return a source list object suitable for {@link Project#getLookup}
*/
public Sources createSources() {
return new SourcesImpl();
}
private final class SourcesImpl implements Sources, PropertyChangeListener, FileChangeListener {
private final ChangeSupport cs = new ChangeSupport(this);
private boolean haveAttachedListeners;
private final Set<File> rootsListenedTo = new HashSet<File>();
/**
* The root URLs which were computed last, keyed by group type.
*/
private final Map<String,List<URL>> lastComputedRoots = new HashMap<String,List<URL>>();
public SourcesImpl() {
evaluator.addPropertyChangeListener(WeakListeners.propertyChange(this, evaluator));
}
public SourceGroup[] getSourceGroups(String type) {
List<SourceGroup> groups = new ArrayList<SourceGroup>();
if (type.equals(Sources.TYPE_GENERIC)) {
List<SourceRoot> roots = new ArrayList<SourceRoot>(principalSourceRoots);
// Always include the project directory itself as a default:
roots.add(new SourceRoot("", ProjectUtils.getInformation(getProject()).getDisplayName(), null, null)); // NOI18N
Map<FileObject,SourceRoot> rootsByDir = new LinkedHashMap<FileObject,SourceRoot>();
// First collect all non-redundant existing roots.
for (SourceRoot r : roots) {
File locF = r.getActualLocation();
if (locF == null) {
continue;
}
listen(locF);
FileObject loc = FileUtil.toFileObject(locF);
if (loc == null) {
continue;
}
if (rootsByDir.containsKey(loc)) {
continue;
}
rootsByDir.put(loc, r);
}
// Remove subroots.
Iterator<FileObject> it = rootsByDir.keySet().iterator();
while (it.hasNext()) {
FileObject loc = it.next();
FileObject parent = loc.getParent();
while (parent != null) {
if (rootsByDir.containsKey(parent)) {
// This is a subroot of something, so skip it.
it.remove();
break;
}
parent = parent.getParent();
}
}
// Everything else is kosher.
for (Map.Entry<FileObject,SourceRoot> entry : rootsByDir.entrySet()) {
groups.add(entry.getValue().toGroup(entry.getKey()));
}
} else {
Set<FileObject> dirs = new HashSet<FileObject>();
for (TypedSourceRoot r : typedSourceRoots) {
if (!r.getType().equals(type)) {
continue;
}
File locF = r.getActualLocation();
if (locF == null) {
continue;
}
listen(locF);
FileObject loc = FileUtil.toFileObject(locF);
if (loc == null) {
continue;
}
if (!dirs.add(loc)) {
// Already had one.
continue;
}
groups.add(r.toGroup(loc));
}
}
// Remember what we computed here so we know whether to fire changes later.
List<URL> rootURLs = new ArrayList<URL>(groups.size());
for (SourceGroup g : groups) {
try {
rootURLs.add(g.getRootFolder().getURL());
} catch (FileStateInvalidException e) {
assert false : e; // should be a valid file object!
}
}
lastComputedRoots.put(type, rootURLs);
return groups.toArray(new SourceGroup[groups.size()]);
}
private synchronized void listen(File rootLocation) {
// #40845. Need to fire changes if a source root is added or removed.
if (rootsListenedTo.add(rootLocation) && /* be lazy */ haveAttachedListeners) {
FileUtil.addFileChangeListener(this, rootLocation);
}
}
public synchronized void addChangeListener(ChangeListener listener) {
if (!haveAttachedListeners) {
haveAttachedListeners = true;
for (File rootLocation : rootsListenedTo) {
FileUtil.addFileChangeListener(this, rootLocation);
}
}
cs.addChangeListener(listener);
}
public void removeChangeListener(ChangeListener listener) {
cs.removeChangeListener(listener);
}
private void maybeFireChange() {
// #47451: check whether anything really changed.
boolean change = false;
// Cannot iterate over entrySet, as the map will be modified by getSourceGroups.
for (String type : new HashSet<String>(lastComputedRoots.keySet())) {
List<URL> previous = new ArrayList<URL>(lastComputedRoots.get(type));
getSourceGroups(type);
List<URL> nue = lastComputedRoots.get(type);
if (!nue.equals(previous)) {
change = true;
break;
}
}
if (change) {
cs.fireChange();
}
}
public void fileFolderCreated(FileEvent fe) {
// Root might have been created on disk.
maybeFireChange();
}
public void fileDataCreated(FileEvent fe) {
maybeFireChange();
}
public void fileDeleted(FileEvent fe) {
// Root might have been deleted.
maybeFireChange();
}
public void fileChanged(FileEvent fe) {
// ignore; generally should not happen (listening to dirs)
}
public void fileRenamed(FileRenameEvent fe) {
maybeFireChange();
}
public void fileAttributeChanged(FileAttributeEvent fe) {
// #164930 - ignore
}
public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
// Properties may have changed so as cause external roots to move etc.
maybeFireChange();
}
}
private final class PropChangeL implements PropertyChangeListener {
public PropChangeL() {}
public void propertyChange(PropertyChangeEvent evt) {
// Some properties changed; external roots might have changed, so check them.
remarkExternalRoots();
}
}
}