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