blob: 647bc0dd07c90e02ff1d35212eea71c07d74cee7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.java.source.ui;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.classpath.GlobalPathRegistry;
import org.netbeans.api.java.classpath.GlobalPathRegistryEvent;
import org.netbeans.api.java.classpath.GlobalPathRegistryListener;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.ui.OpenProjects;
import org.netbeans.modules.java.JavaDataLoader;
import org.netbeans.modules.java.source.indexing.JavaIndex;
import org.netbeans.modules.java.source.usages.ClassIndexManager;
import org.netbeans.modules.java.source.usages.ClassIndexManagerEvent;
import org.netbeans.modules.java.source.usages.ClassIndexManagerListener;
import org.netbeans.modules.parsing.spi.indexing.PathRecognizer;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.URLMapper;
import org.openide.util.*;
/**
* Maintains the 'fast index' which is available very shortly after project open.
* Attaches to the GlobalPathRegistry, and when "pathAdded" event arrives WHILE opening
* projects, creates a Watcher which collects further pathAdded events, until
* the OpenProjects terminates loading and finishes the {@link OpenProjects#openProjects()}.
* future. Watcher runs in a private RequestProcessor and blocks on the Future
* before computing added roots.
* <p/>
* Based on the PROPERTY_OPENED_PROJECTS, a list of <b>newly opened</b> projects is computed
* as a difference between old/new event values. If a single project is added, that project's
* source roots are scheduled for fast-indexing. If more projects is opened, one of them marked
* "main", the main project's roots are added to fast-index. If the system cannot determine
* the 'active' project, nothing happens.
* <p/>
* Because of OpenProjects implementation, the PROPERTY_MAIN_PROJECT property may be changed
* <i>after</i> all projects load and the Future is finished. The Watcher first reschedules itself
* a few milliseconds in the future, to have a chance to receive that event.
*
* @author sdedic
*/
class OpenProjectFastIndex implements ClassIndexManagerListener {
private static final Logger LOG = Logger.getLogger(OpenProjectFastIndex.class.getName());
private static final Strategy STRATEGY;
static {
Strategy s = Strategy.NBFS;
final String propValue = System.getProperty("OpenProjectFastIndex.impl"); //NOI18N
if (propValue != null) {
try {
s = Strategy.valueOf(propValue);
} catch (IllegalArgumentException iae) {
LOG.log(
Level.WARNING,
"Wrong impl: {0}", //NNOI18N
propValue);
}
}
STRATEGY = s;
}
/**
* The default instance, used from TypeProvider and registered in GPR on startup.
*/
private static final OpenProjectFastIndex DEFAULT = new OpenProjectFastIndex();
/**
* Request processor, which executes the task waiting on project completion
*/
private static final RequestProcessor RP = new RequestProcessor("Project prescan initiator", 1);
/**
* PathRecognizers, which provide source path IDs.
*/
private final Lookup.Result<PathRecognizer> recognizers;
/**
* IDs of source paths; other than source changes from GPR will be ignored.
*/
private volatile Set<String> sourcePathIds;
/**
* The current instance of the Watcher runnable.
* The reference will be cleared when the Watcher receives 'project open' event.
*/
private ProjectOpenWatcher watcher;
private final GlobalPathRegistry globalRegistry;
/**
* Roots indexed by the fast indexer. Entries are added as a response to
* GPR change + project open; they are removed when GPR reports source root loss,
* or the root is indexed by the real indexer.
*/
// @GuardedBy(this)
private final Map<FileObject, NameIndex> indexedRoots = new HashMap<FileObject, NameIndex>();
/**
* Will collect roots removed during the scan; when all Watchers finish, the reference
* will be released. The collection is shared between all scheduled Watchers.
*/
// @GuardedBy(self)
private Reference<Set<FileObject>> removedRoots;
/**
* Cached project information. Added at the first request for project icon or name,
* removed when the last source root from the project is removed from fast-index
* (either indexed regularly or removed from the IDE).
*/
// @GuardedBy(self)
private final Map<Project, PI> projectInfoCache = new HashMap<Project, PI>();
/**
* Watcher count
*/
// @GuardedBy(this)
private int watchCount;
private final LookupListener weakLookupL;
private final GlobalPathRegistryListener weakGlobalL;
private final LookupListener lookupL;
private final GlobalPathRegistryListener globalL;
public static OpenProjectFastIndex getDefault() {
return DEFAULT;
}
OpenProjectFastIndex() {
this(false);
recognizers.addLookupListener(weakLookupL);
globalRegistry.addGlobalPathRegistryListener(weakGlobalL);
ClassIndexManager.getDefault().addClassIndexManagerListener(
WeakListeners.create(
ClassIndexManagerListener.class,
this, ClassIndexManager.getDefault()
)
);
}
OpenProjectFastIndex(boolean register) {
this.globalRegistry = GlobalPathRegistry.getDefault();
this.recognizers = Lookup.getDefault().lookupResult(PathRecognizer.class);
updateSourceIds();
weakLookupL = WeakListeners.create(LookupListener.class, lookupL = new LookupListener() {
@Override
public void resultChanged(LookupEvent ev) {
if (ev.getSource() == recognizers) {
updateSourceIds();
}
}
}, recognizers);
weakGlobalL = WeakListeners.create(
GlobalPathRegistryListener.class,
globalL = new GlobalPathRegistryListener() {
@Override
public void pathsAdded(GlobalPathRegistryEvent event) {
if (STRATEGY.isEnabled()) {
Future<Project[]> projects = OpenProjects.getDefault().openProjects();
if (!sourcePathIds.contains(event.getId())) {
LOG.log(Level.FINE, "Non-source paths added: {0}", event.getId());
return;
}
if (projects.isDone()) {
LOG.log(Level.FINE, "Paths added, no project open in progress: {0}", event.getChangedPaths());
return;
}
getWatcher(projects).addChangedPaths(event.getChangedPaths());
}
}
@Override
public void pathsRemoved(GlobalPathRegistryEvent event) {
if (STRATEGY.isEnabled()) {
if (!sourcePathIds.contains(event.getId())) {
LOG.log(Level.FINE, "Non-source removed: {0}", event.getId());
return;
}
Collection<FileObject> roots = getRoots(event.getChangedPaths());
LOG.log(Level.FINE, "Paths removed: {0}", roots);
removeRoots(roots);
}
}
}, globalRegistry);
}
/**
* Removes source roots from Watcher and the index. Should be called
* when the roots become invalid (e.g. removed from IDE), or they are indexed
* by real indexer.
*
* @param roots roots to remove
*/
private synchronized void removeRoots(Collection<FileObject> roots) {
Collection c = removedRoots == null ? null : removedRoots.get();
if (c != null) {
c.addAll(roots);
}
LOG.log(Level.FINE, "Removing roots: {0}", roots);
indexedRoots.keySet().removeAll(roots);
Collection<Project> retainProjects = new ArrayList<Project>(indexedRoots.size());
for (NameIndex ni : indexedRoots.values()) {
retainProjects.add(ni.getProject());
}
synchronized (projectInfoCache) {
Collection ks = projectInfoCache.keySet();
ks.retainAll(retainProjects);
LOG.log(Level.FINEST, "Retained project caches: {0}", ks);
}
}
@Override
public void classIndexAdded(ClassIndexManagerEvent event) {
Collection<FileObject> c = getFileRoots(event.getRoots());
LOG.log(Level.FINE, "Index updated, removing {0} ", c);
removeRoots(c);
}
@Override
public void classIndexRemoved(ClassIndexManagerEvent event) {
Collection<FileObject> c = getFileRoots(event.getRoots());
LOG.log(Level.FINE, "Roots removed from ClassIndexes, removing {0} ", c);
removeRoots(c);
}
private Collection<FileObject> getFileRoots(Iterable<? extends URL> roots) {
Collection c = new ArrayList<FileObject>(5);
for (Iterator<? extends URL> it = roots.iterator(); it.hasNext(); ) {
URL rootURL = it.next();
FileObject fo = URLMapper.findFileObject(rootURL);
if (fo != null) {
c.add(fo);
}
}
return c;
}
private void updateSourceIds() {
Set<String> ids = new HashSet<String>();
for (PathRecognizer r : recognizers.allInstances()) {
Set<String> rids = r.getSourcePathIds();
if (rids != null) {
ids.addAll(rids);
}
}
sourcePathIds = ids;
}
/**
* Returns the current watcher, if still accepting source roots.
* Otherwise, creates AND schedules a new Watcher
*
* @param f Future that the Watcher waits on, before computing source roots.
* @return Watcher instance
*/
private synchronized ProjectOpenWatcher getWatcher(Future f) {
if (watcher != null) {
return watcher;
}
Collection removed = removedRoots == null ? null : removedRoots.get();
if (removed == null) {
removedRoots = new WeakReference(removed = new HashSet<FileObject>());
}
watcher = new ProjectOpenWatcher(f, removed);
watchCount++;
LOG.fine("Starting project open watcher");
RP.post(watcher);
return watcher;
}
/**
* Releases cached project information and clears removed roots list
* when the last live Watcher completes.
* @param w watcher instance that completed work
*/
private synchronized void releaseWatcher(ProjectOpenWatcher w) {
if (--watchCount == 0) {
LOG.fine("Last watcher finished, flushing everything");
removedRoots = null;
synchronized (projectInfoCache) {
projectInfoCache.clear();
}
}
}
// overriden by tests
void updateNameIndexes(Project project, Collection<FileObject> roots) {
IndexBuilder b = new IndexBuilder(project, roots, removedRoots.get());
Map<FileObject, NameIndex> nameIndexes = b.build();
Collection c = removedRoots.get();
assert c != null : "The reference must not expire";
LOG.log(Level.FINE, "Adding roots: {0}", roots);
synchronized (this) {
nameIndexes.keySet().removeAll(c);
this.indexedRoots.putAll(nameIndexes);
}
}
public Map<FileObject, NameIndex> copyIndexes() {
synchronized (this) {
return new HashMap<FileObject, NameIndex>(indexedRoots);
}
}
private static Set<FileObject> getRoots(Set<ClassPath> paths) {
Set<FileObject> sroots = new HashSet<FileObject>();
for (ClassPath cp : paths) {
sroots.addAll(Arrays.asList(cp.getRoots()));
}
return sroots;
}
/**
* This Task will try to select a project from among the opened projects. Instantiated
* when the Listener receives a path change WHILE opening projects - a candidate for
* pre-scanning.
* <p/>
* The class only lives for ONE project open. If another project opens, the running instance
* should be canceled - it will refuse to add additional paths anyway.
*
*/
class ProjectOpenWatcher implements Runnable, PropertyChangeListener {
private int state;
private final Future waitFor;
/**
* The main project, or null if main project change was not reported.
*/
private volatile Project mainProject;
/**
* Source paths, which should be eventually indexed with higher priority upon project open
*/
private Set<FileObject> rootsToIndex = new HashSet<FileObject>();
/**
* Will be filled when PROPERTY_OPENED_PROJECTS is received, with new projects
* reported in that property change.
*/
private Collection<Project> newProjects;
private Collection<FileObject> removedRoots;
public ProjectOpenWatcher(Future waitFor, Collection<FileObject> removedRoots) {
this.waitFor = waitFor;
this.removedRoots = removedRoots;
OpenProjects.getDefault().addPropertyChangeListener(this);
}
public void run() {
int state;
synchronized (this) {
state = this.state++;
}
if (state == 0) {
try {
waitFor.get();
// close this watcher
synchronized (OpenProjectFastIndex.this) {
watcher = null;
synchronized (this) {
this.state++;
}
}
// aargh, this is because PROPERTY_MAIN_PROJECT is fired after the Future has been released
RP.schedule(this, 200, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (ExecutionException ex) {
Exceptions.printStackTrace(ex);
}
} else {
OpenProjects.getDefault().removePropertyChangeListener(this);
try {
// second pass, presumably after MAIN_PROJECT has been fired
Project selected = selectProject(newProjects);
processProject(selected);
} finally {
releaseWatcher(this);
}
}
}
/**
* Preindexes the 'selected' project.
* @param selected
*/
void processProject(Project selected) {
Set<ClassPath> paths;
if (selected == null) {
return;
}
for (Iterator<FileObject> it = rootsToIndex.iterator(); it.hasNext(); ) {
FileObject f = it.next();
// check whether the root has not been indexed yet:
if (JavaIndex.isIndexed(f.toURL())) {
it.remove();
continue;
}
Project p = FileOwnerQuery.getOwner(f);
if (p != selected) {
it.remove();
}
}
if (rootsToIndex.isEmpty()) {
LOG.log(Level.FINE, "Nothing to preindex");
return;
}
// finally, run the preindexing job
updateNameIndexes(selected, rootsToIndex);
}
public boolean addChangedPaths(Set<ClassPath> paths) {
Set<FileObject> sroots = getRoots(paths);
// filter & record source paths that were added for later processing
synchronized (this) {
if (state > 1) {
return false;
}
LOG.log(Level.FINE, "Added source paths: {0}", paths);
rootsToIndex.addAll(sroots);
}
return true;
}
private void matchOpenProjects(Project[] old, Project[] current) {
Map<FileObject, Project> projects = new HashMap<FileObject, Project>();
for (Project p : current) {
projects.put(p.getProjectDirectory(), p);
}
// remove projects, which were already opened before the operation
for (Project p : old) {
projects.remove(p.getProjectDirectory());
}
newProjects = projects.values();
}
private Project selectProject(Collection<Project> newlyOpenedProjects) {
if (newlyOpenedProjects.size() == 1) {
return newlyOpenedProjects.iterator().next();
}
if (newlyOpenedProjects.contains(mainProject)) {
return mainProject;
}
return null;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (OpenProjects.PROPERTY_OPEN_PROJECTS.equals(evt.getPropertyName())) {
matchOpenProjects((Project[])evt.getOldValue(), (Project[])evt.getNewValue());
} else if (OpenProjects.PROPERTY_MAIN_PROJECT.equals(evt.getPropertyName())) {
this.mainProject = (Project)evt.getNewValue();
}
}
}
public String getProjectName(Project p) {
if (p == null) {
return null;
}
return getCacheInfo(p).projectName;
}
public Icon getProjectIcon(Project p) {
if (p == null) {
return null;
}
return getCacheInfo(p).projectIcon;
}
private PI getCacheInfo(Project p) {
synchronized (projectInfoCache) {
PI pi = projectInfoCache.get(p);
if (pi != null) {
return pi;
}
}
ProjectInformation info = ProjectUtils.getInformation( p );
PI pi = new PI();
pi.projectName = info.getDisplayName();
pi.projectIcon = info.getIcon();
synchronized (projectInfoCache) {
projectInfoCache.put(p, pi);
}
return pi;
}
private static final class PI {
Icon projectIcon;
String projectName;
}
/**
* Index of filenames within a certain source root
*/
static class NameIndex {
private final Reference<FileObject> root;
/**
* Newline-delimited filenames
*/
private final String fileNames;
/**
* From 0..size - 1, contains indexes in filenames, where a dir content begins.
* name of particular directory can be found in dirNames at the same index.
* On positions size...size * 2 - 1, this array contains index of parent dir or -1.
*/
private final int[] dirStartPositions;
private final String[] dirNames;
private final int size;
private final Project project;
NameIndex(Project p, FileObject root, String files, ArrayList indices) {
this.project = p;
this.root = new WeakReference(root);
this.size = indices.size();
this.fileNames = files;
this.dirStartPositions = new int[size * 2];
this.dirNames = new String[size];
for (int i = size - 1; i >= 0; i--) {
Object[] data = (Object[])indices.get(i);
dirStartPositions[i] = (Integer)data[1];
dirStartPositions[i + size] = (Integer)data[2];
dirNames[i] = (String)data[0];
}
}
public CharSequence files() {
return fileNames;
}
public CharSequence getFilename(int matchFrom, int matchTo) {
int startLine = fileNames.lastIndexOf('\n', matchFrom); // NOI18N
do {
startLine++;
} while (fileNames.charAt(startLine) == ' ');
int endLine = fileNames.indexOf('\n', matchTo); // NOI18N
return fileNames.subSequence(startLine, endLine);
}
public String findPath(int namePos) {
int atIndex = Arrays.binarySearch(dirStartPositions, 0, size, namePos);
if (atIndex < 0) {
atIndex = -(atIndex + 1) - 1;
}
return appendParents(new StringBuilder(), atIndex).toString();
}
private StringBuilder appendParents(StringBuilder sb, int dirIndex) {
int parentIndex = dirStartPositions[size + dirIndex];
if (parentIndex > 0) {
appendParents(sb, parentIndex).append("."); // NOI18N
}
sb.append(dirNames[dirIndex]);
return sb;
}
public FileObject getRoot() {
return root.get();
}
public Project getProject() {
return project;
}
}
/**
* The index builder; builds file index from dist contents.
*/
static class IndexBuilder {
private Collection<FileObject> rootsToScan;
private StringBuilder filenames;
private Collection<FileObject> removedRoots;
private Project project;
private Map<FileObject, NameIndex> nameIndexes = new HashMap<FileObject, NameIndex>();
private ArrayList dirPositions;
private int charPtr;
public IndexBuilder(Project project, Collection<FileObject> rootsToScan, Collection<FileObject> removedRoots) {
this.rootsToScan = rootsToScan;
this.removedRoots = removedRoots;
this.project = project;
}
private void reset() {
filenames = new StringBuilder();
dirPositions = new ArrayList();
charPtr = 0;
}
public Map<FileObject, NameIndex> build() {
for (FileObject root : rootsToScan) {
synchronized (removedRoots) {
if (removedRoots.contains(root)) {
continue;
}
}
reset();
scanDir(root, -1);
NameIndex ni = new NameIndex(project, root, filenames.toString(), dirPositions);
nameIndexes.put(root, ni);
}
return nameIndexes;
}
private void scanDir(FileObject d, int parentIndex) {
int myIndex = dirPositions.size();
dirPositions.add(new Object[] {
d.getNameExt(),
charPtr,
parentIndex
});
boolean someFile = false;
for (Enumeration<? extends FileObject> en = d.getData(false); en.hasMoreElements(); ) {
FileObject f = en.nextElement();
String fn = f.getName();
if (Utilities.isJavaIdentifier(fn)) {
if (JavaDataLoader.JAVA_MIME_TYPE.equals(f.getMIMEType())) {
someFile = true;
filenames.append(fn).append("\n"); // NOI18N
charPtr += fn.length() + 1;
}
}
}
if (!someFile) {
filenames.append("\n"); // NOI18N
charPtr++;
}
for (Enumeration<? extends FileObject> en = d.getFolders(false); en.hasMoreElements(); ) {
FileObject f = en.nextElement();
if (Utilities.isJavaIdentifier(f.getNameExt())) {
scanDir(f, myIndex);
}
}
}
}
private enum Strategy {
NONE {
@Override
boolean isEnabled() {
return false;
}
},
NBFS {
@Override
boolean isEnabled() {
return true;
}
};
abstract boolean isEnabled();
}
}