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.modules.gsfpath.api.classpath;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.beans.PropertyChangeListener;
046: import java.util.ArrayList;
047: import java.util.Arrays;
048: import java.util.Collections;
049: import java.util.HashMap;
050: import java.util.HashSet;
051: import java.util.LinkedHashSet;
052: import java.util.LinkedList;
053: import java.util.List;
054: import java.util.Map;
055: import java.util.Set;
056: import java.util.logging.Level;
057: import java.util.logging.Logger;
058: import javax.swing.event.ChangeEvent;
059: import javax.swing.event.ChangeListener;
060: import org.netbeans.modules.gsfpath.api.queries.SourceForBinaryQuery;
061: import org.openide.filesystems.FileObject;
062:
063: /**
064: * Maintains a global registry of "interesting" classpaths of various kinds.
065: * You may add and remove different kinds of {@link ClassPath}s to the registry
066: * and listen to changes in them.
067: * <p>
068: * It is permitted to register the same classpath more than once; unregistration
069: * keeps track of the number of registrations so that the operation is symmetric.
070: * However {@link #getPaths} only ever returns one copy of the classpath, and
071: * listeners are only notified the first time a given classpath is added to the
072: * registry, or the last time it is removed.
073: * (Classpath identity is object identity, so there could be multiple paths
074: * returned that at the time share the same list of roots. There may also be
075: * several paths which contain some shared roots.)
076: * </p>
077: * <p>
078: * The registry is not persisted between JVM sessions.
079: * </p>
080: * <div class="nonnormative">
081: * <p>
082: * Intended usage patterns:
083: * </p>
084: * <ol>
085: * <li><p>When a project is opened using
086: * {@link org.netbeans.spi.project.ui.ProjectOpenedHook} it should add any paths
087: * it defines, i.e. paths it might return from a
088: * {@link org.netbeans.modules.gsfpath.spi.classpath.ClassPathProvider}.
089: * When closed it should remove them.</p></li>
090: * <li><p>The <b>Fast Open</b> feature of the editor and other features which
091: * require a global list of relevant sources should use {@link #getSourceRoots} or
092: * the equivalent.</p></li>
093: * <li><p>The <b>Javadoc Index Search</b> feature and <b>View →
094: * Documentation Indices</b> submenu should operate on open Javadoc paths,
095: * meaning that Javadoc corresponding to registered compile and boot classpaths
096: * (according to {@link org.netbeans.modules.gsfpath.api.queries.JavadocForBinaryQuery}).</p></li>
097: * <li><p>Stack trace hyperlinking can use the global list of source paths
098: * to find sources, in case no more specific information about their origin is
099: * available. The same would be true of debugging: if the debugger cannot find
100: * Java-like sources using more precise means ({@link SourceForBinaryQuery}), it
101: * can use {@link #findResource} as a fallback.</p></li>
102: * </ol>
103: * </div>
104: * @author Jesse Glick
105: * @since org.netbeans.modules.gsfpath.api/1 1.4
106: */
107: public final class GlobalPathRegistry {
108:
109: private static final Logger LOG = Logger
110: .getLogger(GlobalPathRegistry.class.getName());
111:
112: private static GlobalPathRegistry DEFAULT = new GlobalPathRegistry();
113:
114: /**
115: * Get the singleton instance of the registry.
116: * @return the default instance
117: */
118: public static GlobalPathRegistry getDefault() {
119: return DEFAULT;
120: }
121:
122: private int resetCount;
123: private final Map<String, List<ClassPath>> paths = new HashMap<String, List<ClassPath>>();
124: private final List<GlobalPathRegistryListener> listeners = new ArrayList<GlobalPathRegistryListener>();
125: private Set<FileObject> sourceRoots = null;
126: private Set<SourceForBinaryQuery.Result> results = new HashSet<SourceForBinaryQuery.Result>();
127:
128: private final ChangeListener resultListener = new SFBQListener();
129:
130: private PropertyChangeListener classpathListener = new PropertyChangeListener() {
131: public void propertyChange(PropertyChangeEvent evt) {
132: synchronized (GlobalPathRegistry.this ) {
133: //Reset cache
134: GlobalPathRegistry.this .resetSourceRootsCache();
135: }
136: }
137: };
138:
139: private GlobalPathRegistry() {
140: }
141:
142: /** for use from unit test */
143: void clear() {
144: paths.clear();
145: listeners.clear();
146: }
147:
148: /**
149: * Find all paths of a certain type.
150: * @param id a classpath type, e.g. {@link ClassPath#SOURCE}
151: * @return an immutable set of all registered {@link ClassPath}s of that type (may be empty but not null)
152: */
153: public synchronized Set<ClassPath> getPaths(String id) {
154: if (id == null) {
155: throw new NullPointerException();
156: }
157: List<ClassPath> l = paths.get(id);
158: if (l != null && !l.isEmpty()) {
159: return Collections
160: .unmodifiableSet(new HashSet<ClassPath>(l));
161: } else {
162: return Collections.<ClassPath> emptySet();
163: }
164: }
165:
166: /**
167: * Register some classpaths of a certain type.
168: * @param id a classpath type, e.g. {@link ClassPath#SOURCE}
169: * @param paths a list of classpaths to add to the registry
170: */
171: public void register(String id, ClassPath[] paths) {
172: if (id == null || paths == null) {
173: throw new NullPointerException();
174: }
175: // Do not log just when firing an event, since there may no listeners.
176: LOG.log(Level.FINE, "registering paths {0} of type {1}",
177: new Object[] { Arrays.asList(paths), id });
178: GlobalPathRegistryEvent evt = null;
179: GlobalPathRegistryListener[] _listeners = null;
180: synchronized (this ) {
181: List<ClassPath> l = this .paths.get(id);
182: if (l == null) {
183: l = new ArrayList<ClassPath>();
184: this .paths.put(id, l);
185: }
186: Set<ClassPath> added = listeners.isEmpty() ? null
187: : new HashSet<ClassPath>();
188: for (ClassPath path : paths) {
189: if (path == null) {
190: throw new NullPointerException(
191: "Null path encountered in "
192: + Arrays.asList(paths)
193: + " of type " + id); // NOI18N
194: }
195: if (added != null && !added.contains(path)
196: && !l.contains(path)) {
197: added.add(path);
198: }
199: if (!l.contains(path)) {
200: path.addPropertyChangeListener(classpathListener);
201: }
202: l.add(path);
203: }
204: if (added != null && !added.isEmpty()) {
205: _listeners = listeners
206: .toArray(new GlobalPathRegistryListener[listeners
207: .size()]);
208: evt = new GlobalPathRegistryEvent(this , id, Collections
209: .unmodifiableSet(added));
210: }
211: // Invalidate cache for getSourceRoots and findResource:
212: resetSourceRootsCache();
213: }
214: if (_listeners != null) {
215: assert evt != null;
216: for (GlobalPathRegistryListener listener : _listeners) {
217: listener.pathsAdded(evt);
218: }
219: }
220: }
221:
222: /**
223: * Unregister some classpaths of a certain type.
224: * @param id a classpath type, e.g. {@link ClassPath#SOURCE}
225: * @param paths a list of classpaths to remove from the registry
226: * @throws IllegalArgumentException if they had not been registered before
227: */
228: public void unregister(String id, ClassPath[] paths)
229: throws IllegalArgumentException {
230: LOG.log(Level.FINE, "unregistering paths {0} of type {1}",
231: new Object[] { Arrays.asList(paths), id });
232: if (id == null || paths == null) {
233: throw new NullPointerException();
234: }
235: GlobalPathRegistryEvent evt = null;
236: GlobalPathRegistryListener[] _listeners = null;
237: synchronized (this ) {
238: List<ClassPath> l = this .paths.get(id);
239: if (l == null) {
240: l = new ArrayList<ClassPath>();
241: }
242: List<ClassPath> l2 = new ArrayList<ClassPath>(l); // in case IAE thrown below
243: Set<ClassPath> removed = listeners.isEmpty() ? null
244: : new HashSet<ClassPath>();
245: for (ClassPath path : paths) {
246: if (path == null) {
247: throw new NullPointerException();
248: }
249: if (!l2.remove(path)) {
250: throw new IllegalArgumentException(
251: "Attempt to remove nonexistent path "
252: + path); // NOI18N
253: }
254: if (removed != null && !removed.contains(path)
255: && !l2.contains(path)) {
256: removed.add(path);
257: }
258: if (!l2.contains(path)) {
259: path
260: .removePropertyChangeListener(classpathListener);
261: }
262: }
263: this .paths.put(id, l2);
264: if (removed != null && !removed.isEmpty()) {
265: _listeners = listeners
266: .toArray(new GlobalPathRegistryListener[listeners
267: .size()]);
268: evt = new GlobalPathRegistryEvent(this , id, Collections
269: .unmodifiableSet(removed));
270: }
271: resetSourceRootsCache();
272: }
273: if (_listeners != null) {
274: assert evt != null;
275: for (GlobalPathRegistryListener listener : _listeners) {
276: listener.pathsRemoved(evt);
277: }
278: }
279: }
280:
281: /**
282: * Add a listener to the registry.
283: * @param l a listener to add
284: */
285: public synchronized void addGlobalPathRegistryListener(
286: GlobalPathRegistryListener l) {
287: if (l == null) {
288: throw new NullPointerException();
289: }
290: listeners.add(l);
291: }
292:
293: /**
294: * Remove a listener to the registry.
295: * @param l a listener to remove
296: */
297: public synchronized void removeGlobalPathRegistryListener(
298: GlobalPathRegistryListener l) {
299: if (l == null) {
300: throw new NullPointerException();
301: }
302: listeners.remove(l);
303: }
304:
305: /**
306: * Convenience method to find all relevant source roots.
307: * This consists of:
308: * <ol>
309: * <li>Roots of all registered {@link ClassPath#SOURCE} paths.
310: * <li>Sources (according to {@link SourceForBinaryQuery}) of all registered
311: * {@link ClassPath#COMPILE} paths.
312: * <li>Sources of all registered {@link ClassPath#BOOT} paths.
313: * </ol>
314: * Order is not significant.
315: * <p>
316: * Currently there is no reliable way to listen for changes in the
317: * value of this method: while you can listen to changes in the paths
318: * mentioned, it is possible for {@link SourceForBinaryQuery} results to
319: * change. In the future a change listener might be added for the value
320: * of the source roots.
321: * </p>
322: * <p>
323: * Note that this method takes no account of package includes/excludes.
324: * </p>
325: * @return an immutable set of <code>FileObject</code> source roots
326: */
327: public Set<FileObject> getSourceRoots() {
328: int currentResetCount;
329: Set<ClassPath> sourcePaths, compileAndBootPaths;
330: synchronized (this ) {
331: if (this .sourceRoots != null) {
332: return this .sourceRoots;
333: }
334: currentResetCount = this .resetCount;
335: sourcePaths = getPaths(ClassPath.SOURCE);
336: compileAndBootPaths = new LinkedHashSet<ClassPath>(
337: getPaths(ClassPath.COMPILE));
338: compileAndBootPaths.addAll(getPaths(ClassPath.BOOT));
339: }
340:
341: Set<FileObject> newSourceRoots = new LinkedHashSet<FileObject>();
342: for (ClassPath sp : sourcePaths) {
343: newSourceRoots.addAll(Arrays.asList(sp.getRoots()));
344: }
345:
346: final List<SourceForBinaryQuery.Result> newResults = new LinkedList<SourceForBinaryQuery.Result>();
347: final ChangeListener tmpResultListener = new SFBQListener();
348: for (ClassPath cp : compileAndBootPaths) {
349: for (ClassPath.Entry entry : cp.entries()) {
350: SourceForBinaryQuery.Result result = SourceForBinaryQuery
351: .findSourceRoots(entry.getURL());
352: result.addChangeListener(tmpResultListener);
353: newResults.add(result);
354: FileObject[] someRoots = result.getRoots();
355: newSourceRoots.addAll(Arrays.asList(someRoots));
356: }
357: }
358:
359: newSourceRoots = Collections.unmodifiableSet(newSourceRoots);
360: synchronized (this ) {
361: if (this .resetCount == currentResetCount) {
362: this .sourceRoots = newSourceRoots;
363: removeTmpSFBQListeners(newResults, tmpResultListener,
364: true);
365: this .results.addAll(newResults);
366: } else {
367: removeTmpSFBQListeners(newResults, tmpResultListener,
368: false);
369: }
370: return newSourceRoots;
371: }
372: }
373:
374: private void removeTmpSFBQListeners(
375: List<? extends SourceForBinaryQuery.Result> results,
376: ChangeListener listener, boolean addListener) {
377: for (SourceForBinaryQuery.Result res : results) {
378: if (addListener) {
379: res.addChangeListener(this .resultListener);
380: }
381: res.removeChangeListener(listener);
382: }
383: }
384:
385: /**
386: * Convenience method to find a particular source file by resource path.
387: * This simply uses {@link #getSourceRoots} to find possible roots and
388: * looks up the resource among them.
389: * In case more than one source root contains the resource, one is chosen
390: * arbitrarily.
391: * As with {@link ClassPath#findResource}, include/exclude lists can affect the result.
392: * @param resource a resource path, e.g. <samp>somepkg/Foo.java</samp>
393: * @return some file found with that path, or null
394: */
395: public FileObject findResource(String resource) {
396: // XXX try to use java.io.File's wherever possible for the search; FileObject is too slow
397: for (ClassPath cp : getPaths(ClassPath.SOURCE)) {
398: FileObject f = cp.findResource(resource);
399: if (f != null) {
400: return f;
401: }
402: }
403: return null;
404: }
405:
406: private synchronized void resetSourceRootsCache() {
407: this .sourceRoots = null;
408: for (SourceForBinaryQuery.Result result : results) {
409: result.removeChangeListener(this .resultListener);
410: }
411: this .resetCount++;
412: }
413:
414: private class SFBQListener implements ChangeListener {
415:
416: public void stateChanged(ChangeEvent event) {
417: synchronized (GlobalPathRegistry.this ) {
418: //Reset cache
419: GlobalPathRegistry.this.resetSourceRootsCache();
420: }
421: }
422: };
423:
424: }
|