0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.netbeans.modules.gsfpath.api.classpath;
0043:
0044: import java.beans.PropertyChangeEvent;
0045: import java.beans.PropertyChangeListener;
0046: import java.beans.PropertyChangeSupport;
0047: import java.io.File;
0048: import java.io.IOException;
0049: import java.lang.ref.Reference;
0050: import java.lang.ref.SoftReference;
0051: import java.lang.ref.WeakReference;
0052: import java.net.URI;
0053: import java.net.URISyntaxException;
0054: import java.net.URL;
0055: import java.text.MessageFormat;
0056: import java.util.ArrayList;
0057: import java.util.Arrays;
0058: import java.util.Collection;
0059: import java.util.Collections;
0060: import java.util.HashSet;
0061: import java.util.LinkedHashSet;
0062: import java.util.List;
0063: import java.util.Map;
0064: import java.util.Set;
0065: import java.util.WeakHashMap;
0066: import java.util.logging.Level;
0067: import java.util.logging.Logger;
0068: import org.netbeans.modules.gsfpath.classpath.ClassPathAccessor;
0069: import org.netbeans.modules.gsfpath.spi.classpath.ClassPathImplementation;
0070: import org.netbeans.modules.gsfpath.spi.classpath.ClassPathProvider;
0071: import org.netbeans.modules.gsfpath.spi.classpath.FilteringPathResourceImplementation;
0072: import org.netbeans.modules.gsfpath.spi.classpath.PathResourceImplementation;
0073: import org.openide.filesystems.FileAttributeEvent;
0074: import org.openide.filesystems.FileChangeListener;
0075: import org.openide.filesystems.FileEvent;
0076: import org.openide.filesystems.FileObject;
0077: import org.openide.filesystems.FileRenameEvent;
0078: import org.openide.filesystems.FileStateInvalidException;
0079: import org.openide.filesystems.FileSystem;
0080: import org.openide.filesystems.FileUtil;
0081: import org.openide.filesystems.URLMapper;
0082: import org.openide.util.Exceptions;
0083: import org.openide.util.Lookup;
0084: import org.openide.util.Utilities;
0085: import org.openide.util.WeakListeners;
0086:
0087: /**
0088: * ClassPath objects should be used to access contents of the ClassPath, searching
0089: * for resources, objects reachable using the ClassPath at runtime. It is intended
0090: * to replace some of the functionality of <link>org.openide.filesystems.Repository</link>.
0091: * <BR>
0092: * ClassPath instances should be used to map from Java-style resource names
0093: * to FileObject (NetBeans-style resource) and vice versa. It should be also used
0094: * whenever the operation requires inspection of development or runtime project
0095: * environment instead. The service supports either searching in the classpath
0096: * resource space, properly hiding resources as the ClassLoader would do at runtime.
0097: * It can effectively say whether a FileObject is within the reach of a ClassPath
0098: * or whether it is <I>reachable</I> (visible to a ClassLoader). One can translate
0099: * filenames to resource names and vice versa.
0100: * <P>
0101: * A client may obtain a ClassPath instance using
0102: * <code>ClassPath.getClassPath(id)</code> static method, where the ID is an
0103: * abstract name for the classpath wanted. There are some predefined classpath
0104: * names predefined as symbolic constants, following individual types of services
0105: * (compiler, debugger, executor). Names are not limited to the listed ones; an extension
0106: * module might add its own private classpath type.
0107: */
0108: public final class ClassPath {
0109:
0110: static {
0111: ClassPathAccessor.DEFAULT = new ClassPathAccessor() {
0112: public ClassPath createClassPath(
0113: ClassPathImplementation spiClasspath) {
0114: return new ClassPath(spiClasspath);
0115: }
0116:
0117: public ClassPathImplementation getClassPathImpl(ClassPath cp) {
0118: return cp == null ? null : cp.impl;
0119: }
0120: };
0121: }
0122:
0123: /**
0124: * Classpath setting for executing things. This type can be used to learn
0125: * runtime time classpath for execution of the file in question.
0126: * <p class="nonnormative">
0127: * It corresponds to the <code>-classpath</code> option to <code>java</code>
0128: * (the Java launcher): i.e. all compiled classes outside the JRE that
0129: * will be needed to run the program, or at least to load a certain class.
0130: * It may also be thought of as corresponding to the list of URLs in a
0131: * <code>URLClassLoader</code> (plus URLs present in parent class loaders
0132: * but excluding the bootstrap and extension class loaders).
0133: * </p>
0134: */
0135: public static final String EXECUTE = "classpath/execute";
0136:
0137: /**
0138: * Classpath for debugging things
0139: * @deprecated Probably useless.
0140: */
0141: @Deprecated
0142: public static final String DEBUG = "classpath/debug";
0143:
0144: /**
0145: * ClassPath for compiling things. This type can be used to learn
0146: * compilation time classpath for the file in question.
0147: * <p class="nonnormative">
0148: * It corresponds to the <code>-classpath</code> option to <code>javac</code>:
0149: * i.e. already-compiled classes which some new sources need to compile against,
0150: * besides what is already in the JRE.
0151: * </p>
0152: */
0153: public static final String COMPILE = "classpath/compile";
0154:
0155: /**
0156: * ClassPath for project sources. This type can be used to learn
0157: * package root of the file in question.
0158: * <div class="nonnormative">
0159: * <p>
0160: * It is similar to the <code>-sourcepath</code> option of <code>javac</code>.
0161: * </p>
0162: * <p>
0163: * For typical source files, the sourcepath will consist of one element:
0164: * the package root of the source file. If more than one package root is
0165: * to be compiled together, all the sources should share a sourcepath
0166: * with multiple roots.
0167: * </p>
0168: * <p>
0169: * Note that each source file for which editor code completion (and similar
0170: * actions) should work should have a classpath of this type.
0171: * </p>
0172: * </div>
0173: * @since org.netbeans.modules.gsfpath.api/1 1.4
0174: */
0175: public static final String SOURCE = "classpath/source";
0176:
0177: /**
0178: * Boot ClassPath of the JDK. This type can be used to learn boot classpath
0179: * which should be used for the file in question.
0180: * <p class="nonnormative">
0181: * It corresponds to the <code>-Xbootclasspath</code> and <code>-Xext</code>
0182: * options to <code>java</code> (the Java launcher): i.e. all compiled
0183: * classes in the JRE that will be needed to run the program.
0184: * It may also be thought of as corresponding to the classes loadable
0185: * by the primordial bootstrap class loader <em>plus</em> the standard
0186: * extension and endorsed-library class loaders; i.e. class loaders lying
0187: * below the regular application startup loader and any custom loaders.
0188: * Generally there ought to be a single boot classpath for the entire
0189: * application.
0190: * </p>
0191: * @since org.netbeans.modules.gsfpath.api/1 1.4
0192: */
0193: public static final String BOOT = "classpath/boot";
0194:
0195: /**
0196: * Name of the "roots" property
0197: */
0198: public static final String PROP_ROOTS = "roots";
0199:
0200: /**
0201: * Name of the "entries" property
0202: */
0203: public static final String PROP_ENTRIES = "entries";
0204:
0205: /**
0206: * Property to be fired when include/exclude set changes.
0207: * @see FilteringPathResourceImplementation
0208: * @since org.netbeans.modules.gsfpath.api/1 1.13
0209: */
0210: public static final String PROP_INCLUDES = "includes";
0211:
0212: private static final Logger LOG = Logger.getLogger(ClassPath.class
0213: .getName());
0214:
0215: private static final Lookup.Result<? extends ClassPathProvider> implementations = Lookup
0216: .getDefault().lookupResult(ClassPathProvider.class);
0217:
0218: private final ClassPathImplementation impl;
0219: private FileObject[] rootsCache;
0220: /**
0221: * Associates entry roots with the matching filter, if there is one.
0222: * XXX not quite right since we could have the same root added twice with two different
0223: * filters. But why would you do that?
0224: */
0225: private Map<FileObject, FilteringPathResourceImplementation> root2Filter = new WeakHashMap<FileObject, FilteringPathResourceImplementation>();
0226: private PropertyChangeListener pListener;
0227: private PropertyChangeListener weakPListener;
0228: private RootsListener rootsListener;
0229: private List<ClassPath.Entry> entriesCache;
0230: private long invalidEntries; //Lamport ordering of events
0231: private long invalidRoots; //Lamport ordering of events
0232:
0233: /**
0234: * Retrieves valid roots of ClassPath, in the proper order.
0235: * If there's an entry in the ClassPath, which cannot be accessed,
0236: * its root is not returned by this method. FileObjects returned
0237: * are all folders.
0238: * Note that this method ignores {@link FilteringPathResourceImplementation includes and excludes}.
0239: * @return array of roots (folders) of the classpath. Never returns
0240: * null.
0241: */
0242: public FileObject[] getRoots() {
0243: long current;
0244: synchronized (this ) {
0245: if (rootsCache != null) {
0246: return this .rootsCache;
0247: }
0248: current = this .invalidRoots;
0249: }
0250: List<ClassPath.Entry> entries = this .entries();
0251: FileObject[] ret;
0252: synchronized (this ) {
0253: if (this .invalidRoots == current) {
0254: if (rootsCache == null || rootsListener == null) {
0255: attachRootsListener();
0256: this .rootsCache = createRoots(entries);
0257: }
0258: ret = this .rootsCache;
0259: } else {
0260: ret = createRoots(entries);
0261: }
0262: }
0263: assert ret != null;
0264: return ret;
0265: }
0266:
0267: private FileObject[] createRoots(final List<ClassPath.Entry> entries) {
0268: List<FileObject> l = new ArrayList<FileObject>();
0269: for (Entry entry : entries) {
0270: RootsListener rootsListener = this .getRootsListener();
0271: if (rootsListener != null) {
0272: rootsListener.addRoot(entry.getURL());
0273: }
0274: FileObject fo = entry.getRoot();
0275: if (fo != null) {
0276: l.add(fo);
0277: root2Filter.put(fo, entry.filter);
0278: }
0279: }
0280: return l.toArray(new FileObject[l.size()]);
0281: }
0282:
0283: /**
0284: * Returns list of classpath entries from the ClassPath definition.
0285: * The implementation must ensure that modifications done to the List are
0286: * banned or at least not reflected in other Lists returned by this ClassPath
0287: * instance. Clients must assume that the returned value is immutable.
0288: * @return list of definition entries (Entry instances)
0289: */
0290: public List<ClassPath.Entry> entries() {
0291: long current;
0292: synchronized (this ) {
0293: if (this .entriesCache != null) {
0294: return this .entriesCache;
0295: }
0296: current = this .invalidEntries;
0297: }
0298: List<? extends PathResourceImplementation> resources = impl
0299: .getResources();
0300: List<ClassPath.Entry> result;
0301: synchronized (this ) {
0302: if (this .invalidEntries == current) {
0303: if (this .entriesCache == null) {
0304: this .entriesCache = createEntries(resources);
0305: }
0306: result = this .entriesCache;
0307: } else {
0308: result = createEntries(resources);
0309: }
0310: }
0311: assert result != null;
0312: return result;
0313: }
0314:
0315: private List<ClassPath.Entry> createEntries(
0316: final List<? extends PathResourceImplementation> resources) {
0317: //The ClassPathImplementation.getResources () should never return
0318: // null but it was not explicitly stated in the javadoc
0319: if (resources == null) {
0320: return Collections.<ClassPath.Entry> emptyList();
0321: } else {
0322: List<ClassPath.Entry> cache = new ArrayList<ClassPath.Entry>();
0323: for (PathResourceImplementation pr : resources) {
0324: pr.removePropertyChangeListener(weakPListener);
0325: pr
0326: .addPropertyChangeListener(weakPListener = WeakListeners
0327: .propertyChange(pListener, pr));
0328: for (URL root : pr.getRoots()) {
0329: cache
0330: .add(new Entry(
0331: root,
0332: pr instanceof FilteringPathResourceImplementation ? (FilteringPathResourceImplementation) pr
0333: : null));
0334: }
0335: }
0336: return Collections.unmodifiableList(cache);
0337: }
0338: }
0339:
0340: private ClassPath(ClassPathImplementation impl) {
0341: if (impl == null)
0342: throw new IllegalArgumentException();
0343: this .impl = impl;
0344: this .pListener = new SPIListener();
0345: this .impl
0346: .addPropertyChangeListener(weakPListener = WeakListeners
0347: .propertyChange(this .pListener, this .impl));
0348: }
0349:
0350: /**
0351: * Returns a FileObject for the specified resource. May return null,
0352: * if the resource does not exist, or is not reachable through
0353: * this ClassPath.<BR>
0354: * If the <i>resourceName</i> identifies a package, this method will
0355: * return the <code>FileObject</code> for the first <I>package fragment</I>
0356: * in the <code>ClassPath</code>.
0357: * {@link FilteringPathResourceImplementation} may cause an actual file
0358: * beneath a registered root to not be returned.
0359: * Note: do not pass names starting with slash to this method.
0360: * @param resourceName name of the resource as it would be passed
0361: * to {@link ClassLoader#getResource}
0362: * @return FileObject for the resource, or null if the resource cannot
0363: * be found in this ClassPath.
0364: */
0365: public final FileObject findResource(String resourceName) {
0366: return findResourceImpl(getRoots(), new int[] { 0 },
0367: parseResourceName(resourceName));
0368: }
0369:
0370: /**
0371: * Gives out an ordered collection containing all FileObjects, which correspond
0372: * to a given ResourceName; only the first one is seen by the ClassLoader
0373: * at runtime or can be linked against. The resource name uses slashes ('/')
0374: * as folder separator and must not start with slash.
0375: * {@link FilteringPathResourceImplementation} may cause an actual file
0376: * beneath a registered root to not be returned.
0377: * @param resourceName resource name
0378: * @return list of resources identified by the given name.
0379: */
0380: public final List<FileObject> findAllResources(String resourceName) {
0381: FileObject[] roots = getRoots();
0382: List<FileObject> l = new ArrayList<FileObject>(roots.length);
0383: int[] idx = new int[] { 0 };
0384: String[] namec = parseResourceName(resourceName);
0385: while (idx[0] < roots.length) {
0386: FileObject f = findResourceImpl(roots, idx, namec);
0387: if (f != null)
0388: l.add(f);
0389: }
0390: return l;
0391: }
0392:
0393: /**
0394: * Creates a suitable resource name for the given FileObject within the
0395: * classpath. The method will return <code>null</code> if the fileobject
0396: * is not underneath any of classpath roots.<BR>
0397: * The returned name uses uses slashes ('/') as folder separators and
0398: * dot ('.') to separate file name and its extension.
0399: * Note that if the file object is in the classpath subtree, but is not reachable
0400: * (it is hidden by some other resource, or {@link FilteringPathResourceImplementation excluded}), the resource name is still returned.
0401: * @return Java-style resource name for the given file object (the empty string for the package root itself), or null if not
0402: * within the classpath
0403: * @param f FileObject whose resource name is requested
0404: */
0405: public final String getResourceName(FileObject f) {
0406: return getResourceName(f, '/', true);
0407: }
0408:
0409: /**
0410: * Computes a resource name for the FileObject, which uses `pathSep' character
0411: * as a directory separator. The resource name can be returned without the file
0412: * extension, if desired. Note that parent folder names are always returned with
0413: * extension, if they have some.
0414: * @param f FileObject whose resource name is requested.
0415: * @param dirSep directory separator character
0416: * @param includeExt whether the FileObject's extension should be included in the result
0417: * @return resource name for the given FileObject (the empty string for the package root itself) or null
0418: */
0419: public final String getResourceName(FileObject f, char dirSep,
0420: boolean includeExt) {
0421: FileObject owner = findOwnerRoot(f);
0422: if (owner == null)
0423: return null;
0424: String partName = FileUtil.getRelativePath(owner, f);
0425: assert partName != null;
0426: if (!includeExt) {
0427: int index = partName.lastIndexOf('.');
0428: if (index >= 0 && index > partName.lastIndexOf('/')) {
0429: partName = partName.substring(0, index);
0430: }
0431: }
0432: if (dirSep != '/') {
0433: partName = partName.replace('/', dirSep);
0434: }
0435: return partName;
0436: }
0437:
0438: /**
0439: * Finds a root in this ClassPath, that owns the given file. File resources, that
0440: * are not reachable (they are hidden by other resources, or {@link FilteringPathResourceImplementation} excluded) are still considered
0441: * to be part of the classpath and "owned" by one of its roots.
0442: * <br>
0443: * <b>Note:</b> This implementation assumes that the FileSystem hosting a classpath root
0444: * contains the entire classpath subtree rooted at that root folder.
0445: * @return classpath root, which hosts the specified resouce. It can return null,
0446: * if the resource is not within the ClassPath contents.
0447: * @param resource resource to find root for.
0448: */
0449: public final FileObject findOwnerRoot(FileObject resource) {
0450: FileObject[] roots = getRoots();
0451: Set<FileObject> rootsSet = new HashSet<FileObject>(Arrays
0452: .asList(roots));
0453: for (FileObject f = resource; f != null; f = f.getParent()) {
0454: if (rootsSet.contains(f)) {
0455: return f;
0456: }
0457: }
0458: return null;
0459: }
0460:
0461: /**
0462: * Checks whether a FileObject lies on this classpath.
0463: * {@link FilteringPathResourceImplementation} is considered.
0464: * @return true, if the parameter is inside one of the classpath subtrees,
0465: * false otherwise.
0466: * @param f the FileObject to check
0467: */
0468: public final boolean contains(FileObject f) {
0469: FileObject root = findOwnerRoot(f);
0470: if (root == null) {
0471: return false;
0472: }
0473: FilteringPathResourceImplementation filter = root2Filter
0474: .get(root);
0475: if (filter == null) {
0476: return true;
0477: }
0478: String path = FileUtil.getRelativePath(root, f);
0479: assert path != null : "could not find " + f + " in " + root;
0480: if (f.isFolder()) {
0481: path += "/"; // NOI18N
0482: }
0483: try {
0484: return filter.includes(root.getURL(), path);
0485: } catch (FileStateInvalidException x) {
0486: throw new AssertionError(x);
0487: }
0488: }
0489:
0490: /**
0491: * Determines if the resource is <i>visible</i> in the classpath,
0492: * that is if the file will be reached when a process attempts to
0493: * load a resource of that name. It will return false when the resource
0494: * is not contained in the classpath, or the resource is {@link FilteringPathResourceImplementation excluded}.
0495: * @param resource the resource whose visibility should be tested
0496: * @return true, if the resource is contained in the classpath and visible;
0497: * false otherwise.
0498: */
0499: public final boolean isResourceVisible(FileObject resource) {
0500: String resourceName = getResourceName(resource);
0501: if (resourceName == null)
0502: return false;
0503: return findResource(resourceName) == resource;
0504: }
0505:
0506: /**
0507: * Adds a property change listener to the bean.
0508: * @param l a listener to add
0509: */
0510: public final synchronized void addPropertyChangeListener(
0511: PropertyChangeListener l) {
0512: attachRootsListener();
0513: propSupport.addPropertyChangeListener(l);
0514: }
0515:
0516: /**
0517: * Removes the listener registered by {@link addPropertyChangeListener}.
0518: * @param l a listener to remove
0519: */
0520: public final void removePropertyChangeListener(
0521: PropertyChangeListener l) {
0522: propSupport.removePropertyChangeListener(l);
0523: }
0524:
0525: /**
0526: * Find the classpath of a given type, if any, defined for a given file.
0527: * <p>This method may return null, if:</p>
0528: * <ul>
0529: * <li>the path type (<code>id</code> parameter) is not recognized
0530: * <li>the path type is not defined for the given file object
0531: * </ul>
0532: * <p>
0533: * Generally you may pass either an individual Java file, or the root of
0534: * a Java package tree, interchangeably, since in most cases all files
0535: * in a given tree will share a single classpath.
0536: * </p>
0537: * <p class="nonnormative">
0538: * Typically classpaths for files are defined by the owning project, but
0539: * there may be other ways classpaths are defined. See {@link ClassPathProvider}
0540: * for more details.
0541: * </p>
0542: * @param f the file, whose classpath settings should be returned (may <em>not</em> be null as of org.netbeans.modules.gsfpath.api/1 1.4)
0543: * @param id the type of the classpath (e.g. {@link #COMPILE})
0544: * @return classpath of the desired type for the given file object, or <code>null</code>, if
0545: * there is no classpath available
0546: * @see ClassPathProvider
0547: */
0548: public static ClassPath getClassPath(FileObject f, String id) {
0549: if (f == null) {
0550: // What else can we do?? Backwards compatibility only.
0551: Thread.dumpStack();
0552: return null;
0553: }
0554: for (ClassPathProvider impl : implementations.allInstances()) {
0555: ClassPath cp = impl.findClassPath(f, id);
0556: if (cp != null) {
0557: LOG.log(Level.FINE,
0558: "getClassPath({0}, {1}) -> {2} from {3}",
0559: new Object[] { f, id, cp, impl });
0560: return cp;
0561: }
0562: }
0563: LOG.log(Level.FINE, "getClassPath({0}, {1}) -> nil",
0564: new Object[] { f, id });
0565: return null;
0566: }
0567:
0568: /**
0569: * Fires a property change event on the specified property, notifying the
0570: * old and new values.
0571: * @param what name of the property
0572: * @param oldV old value
0573: * @param newV new value
0574: */
0575: final void firePropertyChange(final String what, final Object oldV,
0576: final Object newV, final Object propagationId) {
0577: final PropertyChangeEvent event = new PropertyChangeEvent(this ,
0578: what, oldV, newV);
0579: event.setPropagationId(propagationId);
0580: propSupport.firePropertyChange(event);
0581: }
0582:
0583: public String toString() {
0584: return "ClassPath" + entries(); // NOI18N
0585: }
0586:
0587: /**
0588: * Represents an individual entry in the ClassPath. An entry is a description
0589: * of a folder, which is one of the ClassPath roots. Since the Entry does not
0590: * control the folder's lifetime, the folder may be removed and the entry
0591: * becomes invalid. It's also expected that ClassPath implementations may
0592: * use other ClassPath entries as default or base for themselves, so Entries
0593: * may be propagated between ClassPaths.
0594: */
0595: public final class Entry {
0596:
0597: private final URL url;
0598: private FileObject root;
0599: private IOException lastError;
0600: private FilteringPathResourceImplementation filter;
0601:
0602: /**
0603: * Returns the ClassPath instance, which defines/introduces the Entry.
0604: * Note that the return value may differ from the ClassPath instance,
0605: * that produced this Entry from its <code>entries()</code> method.
0606: * @return the ClassPath that defines the entry.
0607: */
0608: public ClassPath getDefiningClassPath() {
0609: return ClassPath.this ;
0610: }
0611:
0612: /**
0613: * The method returns the root folder represented by the Entry.
0614: * If the folder does not exist, or the folder is not readable,
0615: * the method may return null.
0616: * @return classpath entry root folder
0617: */
0618: public FileObject getRoot() {
0619: synchronized (this ) {
0620: if (root != null && root.isValid()) {
0621: return root;
0622: }
0623: }
0624: FileObject _root = URLMapper.findFileObject(this .url);
0625: synchronized (this ) {
0626: if (root == null || !root.isValid()) {
0627: if (_root == null) {
0628: this .lastError = new IOException(
0629: MessageFormat
0630: .format(
0631: "The package root {0} does not exist or can not be read.",
0632: new Object[] { this .url }));
0633: return null;
0634: } else if (_root.isData()) {
0635: throw new IllegalArgumentException(
0636: "Invalid ClassPath root: "
0637: + this .url
0638: + ". The root must be a folder.");
0639: }
0640: root = _root;
0641: }
0642: return root;
0643: }
0644: }
0645:
0646: /**
0647: * @return true, iff the Entry refers to an existing and readable
0648: * folder.
0649: */
0650: public boolean isValid() {
0651: FileObject root = getRoot();
0652: return root != null && root.isValid();
0653: }
0654:
0655: /**
0656: * Retrieves the error condition of the Entry. The method will return
0657: * null, if the <code>getRoot()</code> would return a FileObject.
0658: * @return error condition for this Entry or null if the Entry is OK.
0659: */
0660: public IOException getError() {
0661: IOException error = this .lastError;
0662: this .lastError = null;
0663: return error;
0664: }
0665:
0666: /**
0667: * Returns URL of the class path root.
0668: * This method is generally safer than {@link #getRoot} as
0669: * it can be called even if the root does not currently exist.
0670: * @return URL
0671: * @since org.netbeans.modules.gsfpath.api/1 1.4
0672: */
0673: public URL getURL() {
0674: return this .url;
0675: }
0676:
0677: /**
0678: * Check whether a file is included in this entry.
0679: * @param resource a path relative to @{link #getURL} (must be terminted with <samp>/</samp> if a non-root folder)
0680: * @return true if it is {@link FilteringPathResourceImplementation#includes included}
0681: * @since org.netbeans.modules.gsfpath.api/1 1.13
0682: */
0683: public boolean includes(String resource) {
0684: return filter == null || filter.includes(url, resource);
0685: }
0686:
0687: /**
0688: * Check whether a file is included in this entry.
0689: * @param file a URL beneath @{link #getURL}
0690: * @return true if it is {@link FilteringPathResourceImplementation#includes included}
0691: * @throws IllegalArgumentException in case the argument is not beneath {@link #getURL}
0692: * @since org.netbeans.modules.gsfpath.api/1 1.13
0693: */
0694: public boolean includes(URL file) {
0695: if (!file.toExternalForm().startsWith(url.toExternalForm())) {
0696: throw new IllegalArgumentException(file + " not in "
0697: + url);
0698: }
0699: URI relative;
0700: try {
0701: relative = url.toURI().relativize(file.toURI());
0702: } catch (URISyntaxException x) {
0703: throw new AssertionError(x);
0704: }
0705: assert !relative.isAbsolute() : "could not locate " + file
0706: + " in " + url;
0707: return filter == null
0708: || filter.includes(url, relative.toString());
0709: }
0710:
0711: /**
0712: * Check whether a file is included in this entry.
0713: * @param file a file inside @{link #getRoot}
0714: * @return true if it is {@link FilteringPathResourceImplementation#includes included}
0715: * @throws IllegalArgumentException in case the argument is not beneath {@link #getRoot}, or {@link #getRoot} is null
0716: * @since org.netbeans.modules.gsfpath.api/1 1.13
0717: */
0718: public boolean includes(FileObject file) {
0719: FileObject root = getRoot();
0720: if (root == null) {
0721: throw new IllegalArgumentException("no root in " + url);
0722: }
0723: String path = FileUtil.getRelativePath(root, file);
0724: if (path == null) {
0725: throw new IllegalArgumentException(file + " not in "
0726: + root);
0727: }
0728: if (file.isFolder()) {
0729: path += "/"; // NOI18N
0730: }
0731: return filter == null || filter.includes(url, path);
0732: }
0733:
0734: Entry(URL url, FilteringPathResourceImplementation filter) {
0735: if (url == null)
0736: throw new IllegalArgumentException();
0737: this .url = url;
0738: this .filter = filter;
0739: }
0740:
0741: public String toString() {
0742: return "Entry[" + url + "]"; // NOI18N
0743: }
0744:
0745: @Override
0746: public boolean equals(Object other) {
0747: if (other instanceof ClassPath.Entry) {
0748: return Utilities.compareObjects(
0749: ((ClassPath.Entry) other).url, this .url);
0750: }
0751: return false;
0752: }
0753:
0754: @Override
0755: public int hashCode() {
0756: return this .url == null ? 0 : this .url.hashCode();
0757: }
0758: }
0759:
0760: //-------------------- Implementation details ------------------------//
0761:
0762: private final PropertyChangeSupport propSupport = new PropertyChangeSupport(
0763: this );
0764:
0765: /**
0766: * Attaches the listener to the ClassPath.entries.
0767: * Not synchronized, HAS TO be called from the synchronized block!
0768: */
0769: private void attachRootsListener() {
0770: if (this .rootsListener == null) {
0771: assert this .rootsCache == null;
0772: this .rootsListener = new RootsListener(this );
0773: }
0774: }
0775:
0776: /**
0777: * Returns an array of pairs of strings, first string in the pair is the
0778: * name, the next one is either the extension or null.
0779: */
0780: private static String[] parseResourceName(String name) {
0781: Collection<String> parsed = new ArrayList<String>(
0782: name.length() / 4);
0783: char[] chars = name.toCharArray();
0784: char ch;
0785: int pos = 0;
0786: int dotPos = -1;
0787: int startPos = 0;
0788:
0789: while (pos < chars.length) {
0790: ch = chars[pos];
0791: switch (ch) {
0792: case '.':
0793: dotPos = pos;
0794: break;
0795: case '/':
0796: // end of name component
0797: if (dotPos != -1) {
0798: parsed.add(name.substring(startPos, dotPos));
0799: parsed.add(name.substring(dotPos + 1, pos));
0800: } else {
0801: parsed.add(name.substring(startPos, pos));
0802: parsed.add(null);
0803: }
0804: // reset variables:
0805: startPos = pos + 1;
0806: dotPos = -1;
0807: break;
0808: }
0809: pos++;
0810: }
0811: // if the resource name ends with '/', just ignore the empty component
0812: if (pos > startPos) {
0813: if (dotPos != -1) {
0814: parsed.add(name.substring(startPos, dotPos));
0815: parsed.add(name.substring(dotPos + 1, pos));
0816: } else {
0817: parsed.add(name.substring(startPos, pos));
0818: parsed.add(null);
0819: }
0820: }
0821: if ((parsed.size() % 2) != 0) {
0822: System.err.println("parsed size is not even!!");
0823: System.err.println("input = " + name);
0824: }
0825: return parsed.toArray(new String[parsed.size()]);
0826: }
0827:
0828: /**
0829: * Finds a path underneath the `parent'. Name parts is an array of string pairs,
0830: * the first String in a pair is the basename, the second is the extension or null
0831: * for no extension.
0832: */
0833: private static FileObject findPath(FileObject parent,
0834: String[] nameParts) {
0835: FileObject child;
0836:
0837: for (int i = 0; i < nameParts.length && parent != null; i += 2, parent = child) {
0838: child = parent
0839: .getFileObject(nameParts[i], nameParts[i + 1]);
0840: }
0841: return parent;
0842: }
0843:
0844: /**
0845: * Searches for a resource in one or more roots, gives back the index of
0846: * the first untouched root.
0847: */
0848: private FileObject findResourceImpl(FileObject[] roots,
0849: int[] rootIndex, String[] nameComponents) {
0850: int ridx;
0851: FileObject f = null;
0852: for (ridx = rootIndex[0]; ridx < roots.length && f == null; ridx++) {
0853: f = findPath(roots[ridx], nameComponents);
0854: FilteringPathResourceImplementation filter = root2Filter
0855: .get(roots[ridx]);
0856: if (filter != null) {
0857: try {
0858: if (f != null) {
0859: String path = FileUtil.getRelativePath(
0860: roots[ridx], f);
0861: assert path != null;
0862: if (f.isFolder()) {
0863: path += "/"; // NOI18N
0864: }
0865: if (!filter
0866: .includes(roots[ridx].getURL(), path)) {
0867: f = null;
0868: }
0869: }
0870: } catch (FileStateInvalidException x) {
0871: throw new AssertionError(x);
0872: }
0873: }
0874: }
0875: rootIndex[0] = ridx;
0876: return f;
0877: }
0878:
0879: private static final Reference<ClassLoader> EMPTY_REF = new SoftReference<ClassLoader>(
0880: null);
0881:
0882: private Reference<ClassLoader> refClassLoader = EMPTY_REF;
0883:
0884: /* package private */synchronized void resetClassLoader(
0885: ClassLoader cl) {
0886: if (refClassLoader.get() == cl)
0887: refClassLoader = EMPTY_REF;
0888: }
0889:
0890: /**
0891: * Returns a ClassLoader for loading classes from this ClassPath.
0892: * <p>
0893: * If <code>cache</code> is false, then
0894: * the method will always return a new class loader. If that parameter is true,
0895: * the method may return a loader which survived from a previous call to the same <code>ClassPath</code>.
0896: *
0897: * @param cache true if it is permissible to cache class loaders between calls
0898: * @return class loader which uses the roots in this class path to search for classes and resources
0899: * @since 1.2.1
0900: */
0901: public final synchronized ClassLoader getClassLoader(boolean cache) {
0902: // XXX consider adding ClassLoader and/or InputOutput and/or PermissionCollection params
0903: ClassLoader o = refClassLoader.get();
0904: if (!cache || o == null) {
0905: o = ClassLoaderSupport.create(this );
0906: refClassLoader = new SoftReference<ClassLoader>(o);
0907: }
0908: return o;
0909: }
0910:
0911: private class SPIListener implements PropertyChangeListener {
0912: private Object propIncludesPropagationId;
0913:
0914: public void propertyChange(PropertyChangeEvent evt) {
0915: String prop = evt.getPropertyName();
0916: if (ClassPathImplementation.PROP_RESOURCES.equals(prop)
0917: || PathResourceImplementation.PROP_ROOTS
0918: .equals(prop)) {
0919: synchronized (ClassPath.this ) {
0920: if (rootsListener != null) {
0921: rootsListener.removeAllRoots();
0922: }
0923: entriesCache = null;
0924: rootsCache = null;
0925: invalidEntries++;
0926: invalidRoots++;
0927: }
0928: firePropertyChange(PROP_ENTRIES, null, null, null);
0929: firePropertyChange(PROP_ROOTS, null, null, null);
0930: } else if (FilteringPathResourceImplementation.PROP_INCLUDES
0931: .equals(prop)) {
0932: boolean fire;
0933: synchronized (this ) {
0934: Object id = evt.getPropagationId();
0935: fire = propIncludesPropagationId == null
0936: || !propIncludesPropagationId.equals(id);
0937: propIncludesPropagationId = id;
0938: }
0939: if (fire) {
0940: firePropertyChange(PROP_INCLUDES, null, null, evt
0941: .getPropagationId());
0942: }
0943: }
0944: if (ClassPathImplementation.PROP_RESOURCES.equals(prop)) {
0945: final List<? extends PathResourceImplementation> resources = impl
0946: .getResources();
0947: if (resources == null) {
0948: LOG
0949: .warning("ClassPathImplementation.getResources cannot return null; impl class: "
0950: + impl.getClass().getName());
0951: return;
0952: }
0953: for (PathResourceImplementation pri : resources) {
0954: pri.removePropertyChangeListener(weakPListener);
0955: pri
0956: .addPropertyChangeListener(weakPListener = WeakListeners
0957: .propertyChange(pListener, pri));
0958: }
0959: }
0960: }
0961: }
0962:
0963: private synchronized RootsListener getRootsListener() {
0964: return this .rootsListener;
0965: }
0966:
0967: private static class RootsListener extends WeakReference<ClassPath>
0968: implements FileChangeListener, Runnable {
0969:
0970: private boolean initialized;
0971: private Set<String> roots;
0972:
0973: private RootsListener(ClassPath owner) {
0974: super (owner, Utilities.activeReferenceQueue());
0975: roots = new HashSet<String>();
0976: }
0977:
0978: public void addRoot(URL url) {
0979: if (!isInitialized()) {
0980: FileUtil.addFileChangeListener(this );
0981: setInitialized(true);
0982: }
0983: if ("jar".equals(url.getProtocol())) { //NOI18N
0984: url = FileUtil.getArchiveFile(url);
0985: }
0986: String path = url.getPath();
0987: if (path.endsWith("/")) { //NOI18N
0988: path = path.substring(0, path.length() - 1);
0989: }
0990: roots.add(path);
0991: }
0992:
0993: public void removeRoot(URL url) {
0994: if ("jar".equals(url.getProtocol())) { //NOI18N
0995: url = FileUtil.getArchiveFile(url);
0996: }
0997: String path = url.getPath();
0998: if (path.endsWith("/")) { //NOI18N
0999: path = path.substring(0, path.length() - 1);
1000: }
1001: roots.remove(path);
1002: }
1003:
1004: public void removeAllRoots() {
1005: this .roots.clear();
1006: FileUtil.removeFileChangeListener(this );
1007: initialized = false; //Already synchronized
1008: }
1009:
1010: public void fileFolderCreated(FileEvent fe) {
1011: this .processEvent(fe);
1012: }
1013:
1014: public void fileDataCreated(FileEvent fe) {
1015: this .processEvent(fe);
1016: }
1017:
1018: public void fileChanged(FileEvent fe) {
1019: if (!isInitialized()) {
1020: return; //Cache already cleared
1021: }
1022: String path = getPath(fe.getFile());
1023: if (this .roots.contains(path)) {
1024: ClassPath cp = get();
1025: if (cp != null) {
1026: synchronized (cp) {
1027: cp.rootsCache = null;
1028: cp.invalidRoots++;
1029: this .removeAllRoots(); //No need to listen
1030: }
1031: cp.firePropertyChange(PROP_ROOTS, null, null, null);
1032: }
1033: }
1034: }
1035:
1036: public void fileDeleted(FileEvent fe) {
1037: this .processEvent(fe);
1038: }
1039:
1040: public void fileRenamed(FileRenameEvent fe) {
1041: this .processEvent(fe);
1042: }
1043:
1044: public void fileAttributeChanged(FileAttributeEvent fe) {
1045: }
1046:
1047: public void run() {
1048: if (isInitialized()) {
1049: FileUtil.removeFileChangeListener(this );
1050: }
1051: }
1052:
1053: private void processEvent(FileEvent fe) {
1054: if (!isInitialized()) {
1055: return; //Not interesting, cache already cleared
1056: }
1057: String path = getPath(fe.getFile());
1058: if (path == null)
1059: return;
1060: ClassPath cp = get();
1061: if (cp == null) {
1062: return;
1063: }
1064: boolean fire = false;
1065: synchronized (cp) {
1066: for (String rootPath : roots) {
1067: if (rootPath.startsWith(path)) {
1068: cp.rootsCache = null;
1069: cp.invalidRoots++;
1070: this .removeAllRoots(); //No need to listen
1071: fire = true;
1072: break;
1073: }
1074: }
1075: }
1076: if (fire) {
1077: cp.firePropertyChange(PROP_ROOTS, null, null, null);
1078: }
1079: }
1080:
1081: private static String getPath(FileObject fo) {
1082: if (fo == null)
1083: return null;
1084: try {
1085: URL url = fo.getURL();
1086: String path = url.getPath();
1087: if (path.endsWith("/")) { //NOI18N
1088: path = path.substring(0, path.length() - 1);
1089: }
1090: return path;
1091: } catch (FileStateInvalidException e) {
1092: Exceptions.printStackTrace(e);
1093: return null;
1094: }
1095: }
1096:
1097: private synchronized boolean isInitialized() {
1098: return this .initialized;
1099: }
1100:
1101: private synchronized void setInitialized(boolean newValue) {
1102: this.initialized = newValue;
1103: }
1104: }
1105: }
|