blob: 1e0c98287b4f40b78e96469d263f50c4f3832556 [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.api.java.classpath;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.java.queries.SourceForBinaryQuery;
import org.netbeans.modules.java.classpath.SPIAccessor;
import org.netbeans.spi.java.classpath.ClassPathImplementation;
import org.netbeans.spi.java.classpath.GlobalPathRegistryImplementation;
import org.openide.filesystems.FileObject;
import org.openide.util.Lookup;
import org.openide.util.Parameters;
/**
* Maintains a global registry of "interesting" classpaths of various kinds.
* You may add and remove different kinds of {@link ClassPath}s to the registry
* and listen to changes in them.
* <p>
* It is permitted to register the same classpath more than once; unregistration
* keeps track of the number of registrations so that the operation is symmetric.
* However {@link #getPaths} only ever returns one copy of the classpath, and
* listeners are only notified the first time a given classpath is added to the
* registry, or the last time it is removed.
* ({@link ClassPath} equality is based on equality of the {@link ClassPathImplementation}
* which by default is based on object identity, so there could be multiple paths
* returned that at the time share the same list of roots. There may also be
* several paths which contain some shared roots.)
* </p>
* <p>
* The registry is not persisted between JVM sessions.
* </p>
* <div class="nonnormative">
* <p>
* Intended usage patterns:
* </p>
* <ol>
* <li><p>When a project is opened using
* {@link org.netbeans.spi.project.ui.ProjectOpenedHook} it should add any paths
* it defines, i.e. paths it might return from a
* {@link org.netbeans.spi.java.classpath.ClassPathProvider}.
* When closed it should remove them.</p></li>
* <li><p>The <b>Fast&nbsp;Open</b> feature of the editor and other features which
* require a global list of relevant sources should use {@link #getSourceRoots} or
* the equivalent.</p></li>
* <li><p>The <b>Javadoc&nbsp;Index&nbsp;Search</b> feature and <b>View&nbsp;&#8594;
* Documentation&nbsp;Indices</b> submenu should operate on open Javadoc paths,
* meaning that Javadoc corresponding to registered compile and boot classpaths
* (according to {@link org.netbeans.api.java.queries.JavadocForBinaryQuery}).</p></li>
* <li><p>Stack trace hyperlinking can use the global list of source paths
* to find sources, in case no more specific information about their origin is
* available. The same would be true of debugging: if the debugger cannot find
* Java-like sources using more precise means ({@link SourceForBinaryQuery}), it
* can use {@link #findResource} as a fallback.</p></li>
* </ol>
* </div>
* @author Jesse Glick
* @since org.netbeans.api.java/1 1.4
*/
public final class GlobalPathRegistry {
private static final Logger LOG = Logger.getLogger(GlobalPathRegistry.class.getName());
//@GuardedBy("instances");
private static final Map<GlobalPathRegistryImplementation,Reference<GlobalPathRegistry>> instances =
new WeakHashMap<>();
/**
* Get the singleton instance of the registry.
* <div class="nonnormative">
* <p>
* In environments with scoped global lookup don't cache
* an instance of the {@link GlobalPathRegistry} but rather
* call the {@link GlobalPathRegistry#getDefault} when the
* instance is needed.
* </p>
* </div>
* @return the default instance
*/
@NonNull
public static GlobalPathRegistry getDefault() {
final GlobalPathRegistryImplementation spi = Lookup.getDefault().lookup(GlobalPathRegistryImplementation.class);
if (spi == null) {
throw new IllegalStateException("No GlobalPathRegistryImplementation found in the lookup"); //NOI18N
}
synchronized (instances) {
final Reference<GlobalPathRegistry> apiRef = instances.get(spi);
GlobalPathRegistry api;
if (apiRef == null || (api = apiRef.get()) == null) {
api = new GlobalPathRegistry(spi);
SPIAccessor.getInstance().attachAPI(spi, api);
instances.put(spi, new WeakReference<>(api));
}
return api;
}
}
private final GlobalPathRegistryImplementation spi;
private int resetCount;
private final List<GlobalPathRegistryListener> listeners = new ArrayList<GlobalPathRegistryListener>();
private Set<FileObject> sourceRoots = null;
private Set<SourceForBinaryQuery.Result> results = new HashSet<SourceForBinaryQuery.Result>();
private final ChangeListener resultListener = new SFBQListener ();
private PropertyChangeListener classpathListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
synchronized (GlobalPathRegistry.this) {
//Reset cache
GlobalPathRegistry.this.resetSourceRootsCache ();
}
}
};
private GlobalPathRegistry(@NonNull GlobalPathRegistryImplementation spi) {
Parameters.notNull("spi", spi); //NOI18N
this.spi = spi;
}
/** for use from unit test */
void clear() {
SPIAccessor.getInstance().clear(spi);
listeners.clear();
sourceRoots = null;
}
/**
* Find all paths of a certain type.
* @param id a classpath type, e.g. {@link ClassPath#SOURCE}
* @return an immutable set of all registered {@link ClassPath}s of that type (may be empty but not null)
*/
@NonNull
public Set<ClassPath> getPaths(@NonNull final String id) {
Parameters.notNull("id", id); //NOI18N
synchronized (this) {
return SPIAccessor.getInstance().getPaths(spi, id);
}
}
/**
* Register some classpaths of a certain type.
* @param id a classpath type, e.g. {@link ClassPath#SOURCE}
* @param paths a list of classpaths to add to the registry
*/
public void register(@NonNull final String id, @NonNull final ClassPath[] paths) {
Parameters.notNull("id", id); //NOI18N
Parameters.notNull("paths", paths); //NOI18N
// Do not log just when firing an event, since there may no listeners.
LOG.log(Level.FINE, "registering paths {0} of type {1}", new Object[] {Arrays.asList(paths), id});
Set<ClassPath> added;
GlobalPathRegistryListener[] _listeners = null;
synchronized (this) {
added = SPIAccessor.getInstance().register(spi,id, paths);
for (ClassPath path : added) {
path.addPropertyChangeListener(classpathListener);
}
// Invalidate cache for getSourceRoots and findResource:
resetSourceRootsCache ();
if (LOG.isLoggable(Level.FINER)) {
final Set<ClassPath> l = SPIAccessor.getInstance().getPaths(spi, id);
LOG.log(Level.FINER, "now have {0} paths of type {1}", new Object[] {l.size(), id});
}
if (!listeners.isEmpty() && !added.isEmpty()) {
_listeners = listeners.toArray(new GlobalPathRegistryListener[listeners.size()]);
}
}
if (_listeners != null) {
for (GlobalPathRegistryListener listener : _listeners) {
listener.pathsAdded(new GlobalPathRegistryEvent(this, id, Collections.unmodifiableSet(added)));
}
}
}
/**
* Unregister some classpaths of a certain type.
* @param id a classpath type, e.g. {@link ClassPath#SOURCE}
* @param paths a list of classpaths to remove from the registry
* @throws IllegalArgumentException if they had not been registered before
*/
public void unregister(@NonNull final String id, @NonNull final ClassPath[] paths) throws IllegalArgumentException {
Parameters.notNull("id", id); //NOI18N
Parameters.notNull("paths", paths); //NOI18N
LOG.log(Level.FINE, "unregistering paths {0} of type {1}", new Object[] {Arrays.asList(paths), id});
Set<ClassPath> removed;
GlobalPathRegistryListener[] _listeners = null;
synchronized (this) {
removed = SPIAccessor.getInstance().unregister(spi,id, paths);
for (ClassPath path : removed) {
path.removePropertyChangeListener(classpathListener);
}
resetSourceRootsCache ();
if (LOG.isLoggable(Level.FINER)) {
final Set<ClassPath> l = SPIAccessor.getInstance().getPaths(spi, id);
LOG.log(Level.FINER, "now have {0} paths of type {1}", new Object[] {l.size(), id});
}
if (!listeners.isEmpty() && !removed.isEmpty()) {
_listeners = listeners.toArray(new GlobalPathRegistryListener[listeners.size()]);
}
}
if (_listeners != null) {
for (GlobalPathRegistryListener listener : _listeners) {
listener.pathsRemoved(new GlobalPathRegistryEvent(this, id, Collections.unmodifiableSet(removed)));
}
}
}
/**
* Add a listener to the registry.
* @param l a listener to add
*/
public synchronized void addGlobalPathRegistryListener(GlobalPathRegistryListener l) {
if (l == null) {
throw new NullPointerException();
}
listeners.add(l);
}
/**
* Remove a listener to the registry.
* @param l a listener to remove
*/
public synchronized void removeGlobalPathRegistryListener(GlobalPathRegistryListener l) {
if (l == null) {
throw new NullPointerException();
}
listeners.remove(l);
}
/**
* Convenience method to find all relevant source roots.
* This consists of:
* <ol>
* <li>Roots of all registered {@link ClassPath#SOURCE} paths.
* <li>Sources (according to {@link SourceForBinaryQuery}) of all registered
* {@link ClassPath#COMPILE} paths.
* <li>Sources of all registered {@link ClassPath#BOOT} paths.
* </ol>
* Order is not significant.
* <p>
* Currently there is no reliable way to listen for changes in the
* value of this method: while you can listen to changes in the paths
* mentioned, it is possible for {@link SourceForBinaryQuery} results to
* change. In the future a change listener might be added for the value
* of the source roots.
* </p>
* <p>
* Note that this method takes no account of package includes/excludes.
* </p>
* @return an immutable set of <code>FileObject</code> source roots
*/
public Set<FileObject> getSourceRoots() {
int currentResetCount;
Set<ClassPath> sourcePaths, compileAndBootPaths;
synchronized (this) {
if (this.sourceRoots != null) {
return this.sourceRoots;
}
currentResetCount = this.resetCount;
sourcePaths = getPaths(ClassPath.SOURCE);
compileAndBootPaths = new LinkedHashSet<ClassPath>(getPaths(ClassPath.COMPILE));
compileAndBootPaths.addAll(getPaths(ClassPath.BOOT));
}
Set<FileObject> newSourceRoots = new LinkedHashSet<FileObject>();
for (ClassPath sp : sourcePaths) {
newSourceRoots.addAll(Arrays.asList(sp.getRoots()));
}
final List<SourceForBinaryQuery.Result> newResults = new LinkedList<SourceForBinaryQuery.Result> ();
final ChangeListener tmpResultListener = new SFBQListener ();
for (ClassPath cp : compileAndBootPaths) {
for (ClassPath.Entry entry : cp.entries()) {
SourceForBinaryQuery.Result result = SourceForBinaryQuery.findSourceRoots(entry.getURL());
result.addChangeListener(tmpResultListener);
newResults.add (result);
FileObject[] someRoots = result.getRoots();
newSourceRoots.addAll(Arrays.asList(someRoots));
}
}
newSourceRoots = Collections.unmodifiableSet(newSourceRoots);
synchronized (this) {
if (this.resetCount == currentResetCount) {
this.sourceRoots = newSourceRoots;
removeTmpSFBQListeners (newResults, tmpResultListener, true);
this.results.addAll (newResults);
}
else {
removeTmpSFBQListeners (newResults, tmpResultListener, false);
}
return newSourceRoots;
}
}
private void removeTmpSFBQListeners (List<? extends SourceForBinaryQuery.Result> results, ChangeListener listener, boolean addListener) {
for (SourceForBinaryQuery.Result res : results) {
if (addListener) {
res.addChangeListener (this.resultListener);
}
res.removeChangeListener(listener);
}
}
/**
* Convenience method to find a particular source file by resource path.
* This simply uses {@link #getSourceRoots} to find possible roots and
* looks up the resource among them.
* In case more than one source root contains the resource, one is chosen
* arbitrarily.
* As with {@link ClassPath#findResource}, include/exclude lists can affect the result.
* @param resource a resource path, e.g. <samp>somepkg/Foo.java</samp>
* @return some file found with that path, or null
*/
public FileObject findResource(String resource) {
for (ClassPath cp : getPaths(ClassPath.SOURCE)) {
FileObject f = cp.findResource(resource);
if (f != null) {
return f;
}
}
for (FileObject root : getSourceRoots()) {
FileObject f = root.getFileObject(resource);
if (f != null) {
// Make sure it is not from one of the above, since they control incl/excl.
for (ClassPath cp : getPaths(ClassPath.SOURCE)) {
if (cp.findOwnerRoot(f) != null) {
return null;
}
}
return f;
}
}
return null;
}
private synchronized void resetSourceRootsCache () {
this.sourceRoots = null;
for (Iterator< ? extends SourceForBinaryQuery.Result> it = results.iterator(); it.hasNext();) {
SourceForBinaryQuery.Result result = it.next();
it.remove();
result.removeChangeListener(this.resultListener);
}
this.resetCount++;
}
private class SFBQListener implements ChangeListener {
public void stateChanged (ChangeEvent event) {
synchronized (GlobalPathRegistry.this) {
//Reset cache
GlobalPathRegistry.this.resetSourceRootsCache ();
}
}
};
/**
* Testability
* Used by unit GlobalPathRegistryTest
* @return set of {@link SourceForBinaryQuery.Result} the {@link GlobalPathRegistry}
* listens on.
*/
Set<? extends SourceForBinaryQuery.Result> getResults () {
return Collections.unmodifiableSet(this.results);
}
}