All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.api.java.classpath.GlobalPathRegistry Maven / Gradle / Ivy

The newest version!
/*
 * 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.net.URL;
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.
 * 

* 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.) *

*

* The registry is not persisted between JVM sessions. *

*
*

* Intended usage patterns: *

*
    *
  1. When a project is opened using * 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.

  2. *
  3. The Fast Open feature of the editor and other features which * require a global list of relevant sources should use {@link #getSourceRoots} or * the equivalent.

  4. *
  5. The Javadoc Index Search feature and View → * Documentation Indices submenu should operate on open Javadoc paths, * meaning that Javadoc corresponding to registered compile and boot classpaths * (according to JavadocForBinaryQuery).

  6. *
  7. 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.

  8. *
*
* @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> instances = new WeakHashMap<>(); /** * Get the singleton instance of the registry. *
*

* 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. *

*
* @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 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 listeners = new ArrayList(); private Set sourceRoots = null; private Set results = new HashSet(); 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 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 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 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[0]); } } 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 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 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[0]); } } 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: *
    *
  1. Roots of all registered {@link ClassPath#SOURCE} paths. *
  2. Sources (according to {@link SourceForBinaryQuery}) of all registered * {@link ClassPath#COMPILE} paths. *
  3. Sources of all registered {@link ClassPath#BOOT} paths. *
* Order is not significant. *

* 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. *

*

* Note that this method takes no account of package includes/excludes. *

* @return an immutable set of FileObject source roots */ public Set getSourceRoots() { int currentResetCount; Set sourcePaths, compileAndBootPaths; synchronized (this) { if (this.sourceRoots != null) { return this.sourceRoots; } currentResetCount = this.resetCount; sourcePaths = getPaths(ClassPath.SOURCE); compileAndBootPaths = new LinkedHashSet(getPaths(ClassPath.COMPILE)); compileAndBootPaths.addAll(getPaths(ClassPath.BOOT)); } Set newSourceRoots = new LinkedHashSet(); for (ClassPath sp : sourcePaths) { newSourceRoots.addAll(Arrays.asList(sp.getRoots())); } final List newResults = new LinkedList (); final Set seenEntryURL = new HashSet<>(); final ChangeListener tmpResultListener = new SFBQListener (); for (ClassPath cp : compileAndBootPaths) { for (ClassPath.Entry entry : cp.entries()) { URL url = entry.getURL(); String urlKey = url.toString(); if (!seenEntryURL.add(urlKey)) { //we have already processed this binary root, skip continue; } SourceForBinaryQuery.Result result = SourceForBinaryQuery.findSourceRoots(url); 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 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. somepkg/Foo.java * @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 getResults () { return Collections.unmodifiableSet(this.results); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy