| /* |
| * 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.beans.PropertyChangeSupport; |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.ref.Reference; |
| import java.lang.ref.SoftReference; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.WeakHashMap; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import org.netbeans.api.annotations.common.CheckForNull; |
| import org.netbeans.api.annotations.common.NonNull; |
| import org.netbeans.api.annotations.common.NullAllowed; |
| import org.netbeans.modules.java.classpath.ClassPathAccessor; |
| import org.netbeans.modules.java.classpath.SimplePathResourceImplementation; |
| import org.netbeans.spi.java.classpath.ClassPathImplementation; |
| import org.netbeans.spi.java.classpath.ClassPathProvider; |
| import org.netbeans.spi.java.classpath.FilteringPathResourceImplementation; |
| import org.netbeans.spi.java.classpath.FlaggedClassPathImplementation; |
| import org.netbeans.spi.java.classpath.PathResourceImplementation; |
| import org.netbeans.spi.java.classpath.support.ClassPathSupport; |
| import org.openide.filesystems.FileChangeAdapter; |
| import org.openide.filesystems.FileEvent; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileRenameEvent; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.filesystems.URLMapper; |
| import org.openide.util.BaseUtilities; |
| import org.openide.util.Lookup; |
| import org.openide.util.Pair; |
| import org.openide.util.Parameters; |
| import org.openide.util.WeakListeners; |
| |
| /** |
| * ClassPath objects should be used to access contents of the ClassPath, searching |
| * for resources, objects reachable using the ClassPath at runtime. It is intended |
| * to replace some of the functionality of {@link org.openide.filesystems.Repository}. |
| * <BR> |
| * ClassPath instances should be used to map from Java-style resource names |
| * to FileObject (NetBeans-style resource) and vice versa. It should be also used |
| * whenever the operation requires inspection of development or runtime project |
| * environment instead. The service supports either searching in the classpath |
| * resource space, properly hiding resources as the ClassLoader would do at runtime. |
| * It can effectively say whether a FileObject is within the reach of a ClassPath |
| * or whether it is <I>reachable</I> (visible to a ClassLoader). One can translate |
| * filenames to resource names and vice versa. |
| * <P> |
| * A client may obtain a ClassPath instance using |
| * <code>ClassPath.getClassPath(id)</code> static method, where the ID is an |
| * abstract name for the classpath wanted. There are some predefined classpath |
| * names predefined as symbolic constants, following individual types of services |
| * (compiler, debugger, executor). Names are not limited to the listed ones; an extension |
| * module might add its own private classpath type. |
| */ |
| |
| public final class ClassPath { |
| |
| static { |
| ClassPathAccessor.setDefault(new ClassPathAccessor() { |
| public ClassPath createClassPath(ClassPathImplementation spiClasspath) { |
| return new ClassPath(spiClasspath); |
| } |
| public ClassPathImplementation getClassPathImpl(ClassPath cp) { |
| return cp == null ? null : cp.impl; |
| } |
| }); |
| } |
| |
| /** |
| * Classpath setting for executing things. This type can be used to learn |
| * runtime time classpath for execution of the file in question. |
| * <p class="nonnormative"> |
| * It corresponds to the <code>-classpath</code> option to <code>java</code> |
| * (the Java launcher): i.e. all compiled classes outside the JRE that |
| * will be needed to run the program, or at least to load a certain class. |
| * It may also be thought of as corresponding to the list of URLs in a |
| * <code>URLClassLoader</code> (plus URLs present in parent class loaders |
| * but excluding the bootstrap and extension class loaders). |
| * </p> |
| */ |
| public static final String EXECUTE = "classpath/execute"; |
| |
| /** |
| * Classpath for debugging things |
| * @deprecated Probably useless. |
| */ |
| @Deprecated |
| public static final String DEBUG = "classpath/debug"; |
| |
| /** |
| * ClassPath for compiling things. This type can be used to learn |
| * compilation time classpath for the file in question. |
| * <p class="nonnormative"> |
| * It corresponds to the <code>-classpath</code> option to <code>javac</code>: |
| * i.e. already-compiled classes which some new sources need to compile against, |
| * besides what is already in the JRE. |
| * </p> |
| */ |
| public static final String COMPILE = "classpath/compile"; |
| |
| /** |
| * ClassPath for project sources. This type can be used to learn |
| * package root of the file in question. |
| * <div class="nonnormative"> |
| * <p> |
| * It is similar to the <code>-sourcepath</code> option of <code>javac</code>. |
| * </p> |
| * <p> |
| * For typical source files, the sourcepath will consist of one element: |
| * the package root of the source file. If more than one package root is |
| * to be compiled together, all the sources should share a sourcepath |
| * with multiple roots. |
| * </p> |
| * <p> |
| * Note that each source file for which editor code completion (and similar |
| * actions) should work should have a classpath of this type. |
| * </p> |
| * </div> |
| * @since org.netbeans.api.java/1 1.4 |
| */ |
| public static final String SOURCE = "classpath/source"; |
| |
| /** |
| * Boot ClassPath of the JDK. This type can be used to learn boot classpath |
| * which should be used for the file in question. |
| * <p class="nonnormative"> |
| * It corresponds to the <code>-Xbootclasspath</code> and <code>-Xext</code> |
| * options to <code>java</code> (the Java launcher): i.e. all compiled |
| * classes in the JRE that will be needed to run the program. |
| * It may also be thought of as corresponding to the classes loadable |
| * by the primordial bootstrap class loader <em>plus</em> the standard |
| * extension and endorsed-library class loaders; i.e. class loaders lying |
| * below the regular application startup loader and any custom loaders. |
| * Generally there ought to be a single boot classpath for the entire |
| * application. |
| * </p> |
| * @since org.netbeans.api.java/1 1.4 |
| */ |
| public static final String BOOT = "classpath/boot"; |
| |
| /** |
| * Name of the "roots" property |
| */ |
| public static final String PROP_ROOTS = "roots"; |
| |
| /** |
| * Name of the "entries" property |
| */ |
| public static final String PROP_ENTRIES = "entries"; |
| |
| /** |
| * Name of the "flags" property |
| * @since 1.43 |
| */ |
| public static final String PROP_FLAGS = "flags"; |
| |
| /** |
| * Property to be fired when include/exclude set changes. |
| * @see FilteringPathResourceImplementation |
| * @since org.netbeans.api.java/1 1.13 |
| */ |
| public static final String PROP_INCLUDES = "includes"; |
| |
| /** |
| * The empty ClassPath. |
| * Contains no entries and never fires events. |
| * @since 1.24 |
| */ |
| public static final ClassPath EMPTY = new ClassPath(); |
| |
| private static final String URL_EMBEDDING = "!/"; //NOI18N |
| private static final Logger LOG = Logger.getLogger(ClassPath.class.getName()); |
| |
| private static final AtomicReference<Lookup.Result<? extends ClassPathProvider>> implementations = |
| new AtomicReference<>(); |
| |
| private final ClassPathImplementation impl; |
| private final Throwable caller; |
| private FileObject[] rootsCache; |
| /** |
| * Associates entry roots with the matching filter, if there is one. |
| * XXX not quite right since we could have the same root added twice with two different |
| * filters. But why would you do that? |
| */ |
| private Map<FileObject,FilteringPathResourceImplementation> root2Filter = new WeakHashMap<FileObject,FilteringPathResourceImplementation>(); |
| private PropertyChangeListener pListener; |
| private final List<Object[]> weakPListeners = new LinkedList<Object[]>(); //todo: Replace with Pair<PathResourceImplementation,PropertyChangeListener> when Pair available |
| private RootsListener rootsListener; |
| private List<ClassPath.Entry> entriesCache; |
| private long invalidEntries; //Lamport ordering of events |
| private long invalidRoots; //Lamport ordering of events |
| |
| /** |
| * Retrieves valid roots of ClassPath, in the proper order. |
| * If there's an entry in the ClassPath, which cannot be accessed, |
| * its root is not returned by this method. FileObjects returned |
| * are all folders. |
| * Note that this method ignores {@link FilteringPathResourceImplementation includes and excludes}. |
| * @return array of roots (folders) of the classpath. Never returns |
| * null. |
| */ |
| public FileObject[] getRoots() { |
| long current; |
| synchronized (this) { |
| if (rootsCache != null) { |
| return rootsCache.clone(); |
| } |
| current = this.invalidRoots; |
| } |
| final List<ClassPath.Entry> entries = this.entries(); |
| final List<Pair<ClassPath.Entry,Pair<FileObject,File>>> rootPairs = createRoots(entries); |
| FileObject[] roots = rootPairs.stream() |
| .map((p) -> p.second().first()) |
| .filter((fo) -> fo != null) |
| .toArray((size) -> new FileObject[size]); |
| synchronized (this) { |
| if (this.invalidRoots == current) { |
| if (rootsCache == null || rootsListener == null) { |
| attachRootsListener(); |
| listenOnRoots(rootPairs); |
| this.rootsCache = roots.clone(); |
| } else { |
| roots = rootsCache.clone(); |
| } |
| } |
| } |
| return roots; |
| } |
| |
| private File getFile(final ClassPath.Entry entry) { |
| URL url = entry.getURL(); |
| if ("jar".equals(url.getProtocol())) { //NOI18N |
| url = FileUtil.getArchiveFile(url); |
| if (url == null) { |
| LOG.log( |
| Level.WARNING, |
| "Invalid classpath root: {0} provided by: {1}", //NOI18N |
| new Object[]{ |
| entry.getURL(), |
| impl |
| }); |
| return null; |
| } |
| } |
| if (!"file".equals(url.getProtocol())) { // NOI18N |
| // Try to convert nbinst to file |
| FileObject fileFo = URLMapper.findFileObject(url); |
| if (fileFo != null) { |
| URL external = URLMapper.findURL(fileFo, URLMapper.EXTERNAL); |
| if (external != null) { |
| url = external; |
| } |
| } |
| } |
| try { |
| //todo: Ignore non file urls, we can try to url->fileobject->url |
| //if it becomes a file. |
| if ("file".equals(url.getProtocol())) { //NOI18N |
| return FileUtil.normalizeFile(BaseUtilities.toFile(url.toURI())); |
| } |
| } catch (IllegalArgumentException e) { |
| LOG.log(Level.WARNING, "Unexpected URL <{0}>: {1}", new Object[] {url, e}); |
| //pass |
| } catch (URISyntaxException e) { |
| LOG.log(Level.WARNING, "Invalid URL: {0}", url); |
| //pass |
| } |
| return null; |
| } |
| |
| private List<Pair<ClassPath.Entry,Pair<FileObject,File>>> createRoots (final List<ClassPath.Entry> entries) { |
| final List<Pair<ClassPath.Entry,Pair<FileObject,File>>> l = new ArrayList<> (); |
| for (Entry entry : entries) { |
| File file = null; |
| FileObject fo = entry.getRoot(); |
| if (fo != null) { |
| FileObject fileFo = FileUtil.getArchiveFile(fo); |
| if (fileFo == null) { |
| fileFo = fo; |
| } |
| file = FileUtil.toFile(fileFo); |
| } |
| if (file == null) { |
| file = getFile(entry); |
| } |
| l.add(Pair.of(entry,Pair.of(fo,file))); |
| } |
| return l; |
| } |
| |
| private void listenOnRoots (final List<Pair<ClassPath.Entry,Pair<FileObject,File>>> roots) { |
| final Set<File> listenOn = new HashSet<>(); |
| for (Pair<ClassPath.Entry,Pair<FileObject,File>> p : roots) { |
| final ClassPath.Entry entry = p.first(); |
| final FileObject fo = p.second().first(); |
| if (fo != null) { |
| root2Filter.put(fo, entry.filter); |
| } |
| final File file = p.second().second(); |
| if (file != null) { |
| listenOn.add(file); |
| } |
| } |
| final RootsListener rL = this.getRootsListener(); |
| if (rL != null) { |
| rL.addRoots (listenOn); |
| } |
| } |
| |
| /** |
| * Returns list of classpath entries from the ClassPath definition. |
| * The implementation must ensure that modifications done to the List are |
| * banned or at least not reflected in other Lists returned by this ClassPath |
| * instance. Clients must assume that the returned value is immutable. |
| * @return list of definition entries (Entry instances) |
| */ |
| public List<ClassPath.Entry> entries() { |
| long current; |
| synchronized (this) { |
| if (this.entriesCache != null) { |
| return this.entriesCache; |
| } |
| current = this.invalidEntries; |
| } |
| List<? extends PathResourceImplementation> resources = impl.getResources(); |
| if (resources == null) |
| throw new NullPointerException ( |
| "ClassPathImplementation.getResources() returned null. ClassPathImplementation.class: " |
| + impl.getClass ().toString () + " ClassPathImplementation: " + impl.toString () |
| ); |
| final List<Object[]> snapshot = new ArrayList<Object[]>(); |
| for (PathResourceImplementation pr : resources) { |
| snapshot.add(new Object[] {pr, pr.getRoots()}); |
| } |
| List<ClassPath.Entry> result; |
| synchronized (this) { |
| if (this.invalidEntries == current) { |
| if (this.entriesCache == null) { |
| this.entriesCache = createEntries (snapshot); |
| } |
| result = this.entriesCache; |
| } |
| else { |
| result = createEntries (snapshot); |
| } |
| } |
| assert result != null; |
| return result; |
| } |
| |
| //@GuardedBy("this") |
| private List<ClassPath.Entry> createEntries (final List<Object[]> resources) { |
| List<ClassPath.Entry> cache = new ArrayList<ClassPath.Entry> (); |
| for (final Iterator<Object[]> it = weakPListeners.iterator(); it.hasNext();) { |
| final Object[] rwp = it.next(); |
| it.remove(); |
| ((PathResourceImplementation)rwp[0]).removePropertyChangeListener((PropertyChangeListener)rwp[1]); |
| } |
| assert weakPListeners.isEmpty(); |
| for (Object[] pair : resources) { |
| PathResourceImplementation pr = (PathResourceImplementation) pair[0]; |
| URL[] roots = (URL[]) pair[1]; |
| final PropertyChangeListener weakPListener = WeakListeners.propertyChange(pListener, pr); |
| pr.addPropertyChangeListener(weakPListener); |
| weakPListeners.add(new Object[]{pr, weakPListener}); |
| for (URL root : roots) { |
| if (!(pr instanceof SimplePathResourceImplementation)) { // ctor already checks these things |
| SimplePathResourceImplementation.verify(root, " From: " + pr.getClass().getName(), caller); |
| } |
| cache.add(new Entry(root, |
| pr instanceof FilteringPathResourceImplementation ? (FilteringPathResourceImplementation) pr : null)); |
| } |
| } |
| return Collections.unmodifiableList(cache); |
| } |
| |
| private ClassPath (ClassPathImplementation impl) { |
| if (impl == null) |
| throw new IllegalArgumentException (); |
| this.propSupport = new PropertyChangeSupport(this); |
| this.impl = impl; |
| this.pListener = new SPIListener (); |
| this.impl.addPropertyChangeListener (WeakListeners.propertyChange(this.pListener, this.impl)); |
| caller = new IllegalArgumentException(); |
| } |
| |
| private ClassPath() { |
| this.propSupport = new PropertyChangeSupport(this) { |
| @Override |
| public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {} |
| @Override |
| public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {} |
| @Override |
| public void firePropertyChange(PropertyChangeEvent evt) {} |
| }; |
| this.impl = new ClassPathImplementation() { |
| public List<? extends PathResourceImplementation> getResources() { |
| return Collections.emptyList(); |
| } |
| public void addPropertyChangeListener(PropertyChangeListener listener) {} |
| public void removePropertyChangeListener(PropertyChangeListener listener) {} |
| }; |
| this.pListener = new SPIListener (); |
| caller = new IllegalArgumentException(); |
| } |
| |
| /** |
| * Returns a FileObject for the specified resource. May return null, |
| * if the resource does not exist, or is not reachable through |
| * this ClassPath.<BR> |
| * If the <i>resourceName</i> identifies a package, this method will |
| * return the <code>FileObject</code> for the first <I>package fragment</I> |
| * in the <code>ClassPath</code>. |
| * {@link FilteringPathResourceImplementation} may cause an actual file |
| * beneath a registered root to not be returned. |
| * Note: do not pass names starting with slash to this method. |
| * @param resourceName name of the resource as it would be passed |
| * to {@link ClassLoader#getResource} |
| * @return FileObject for the resource, or null if the resource cannot |
| * be found in this ClassPath. |
| */ |
| public final FileObject findResource(String resourceName) { |
| return findResourceImpl(getRoots(), new int[] {0}, parseResourceName(resourceName)); |
| } |
| |
| /** |
| * Gives out an ordered collection containing all FileObjects, which correspond |
| * to a given ResourceName; only the first one is seen by the ClassLoader |
| * at runtime or can be linked against. The resource name uses slashes ('/') |
| * as folder separator and must not start with slash. |
| * {@link FilteringPathResourceImplementation} may cause an actual file |
| * beneath a registered root to not be returned. |
| * @param resourceName resource name |
| * @return list of resources identified by the given name. |
| */ |
| public final List<FileObject> findAllResources(String resourceName) { |
| FileObject[] roots = getRoots(); |
| List<FileObject> l = new ArrayList<FileObject>(roots.length); |
| int[] idx = new int[] { 0 }; |
| String[] namec = parseResourceName(resourceName); |
| while (idx[0] < roots.length) { |
| FileObject f = findResourceImpl(roots, idx, namec); |
| if (f != null) |
| l.add(f); |
| } |
| return l; |
| } |
| |
| /** |
| * Creates a suitable resource name for the given FileObject within the |
| * classpath. The method will return <code>null</code> if the fileobject |
| * is not underneath any of classpath roots.<BR> |
| * The returned name uses uses slashes ('/') as folder separators and |
| * dot ('.') to separate file name and its extension. |
| * Note that if the file object is in the classpath subtree, but is not reachable |
| * (it is hidden by some other resource, or {@link FilteringPathResourceImplementation excluded}), the resource name is still returned. |
| * @return Java-style resource name for the given file object (the empty string for the package root itself), or null if not |
| * within the classpath |
| * @param f FileObject whose resource name is requested |
| */ |
| public final String getResourceName(FileObject f) { |
| return getResourceName(f, '/', true); |
| } |
| |
| /** |
| * Computes a resource name for the FileObject, which uses `pathSep' character |
| * as a directory separator. The resource name can be returned without the file |
| * extension, if desired. Note that parent folder names are always returned with |
| * extension, if they have some. |
| * @param f FileObject whose resource name is requested. |
| * @param dirSep directory separator character |
| * @param includeExt whether the FileObject's extension should be included in the result |
| * @return resource name for the given FileObject (the empty string for the package root itself) or null |
| */ |
| public final String getResourceName(FileObject f, char dirSep, boolean includeExt) { |
| FileObject owner = findOwnerRoot(f); |
| if (owner == null) |
| return null; |
| String partName = FileUtil.getRelativePath(owner, f); |
| assert partName != null; |
| if (!includeExt) { |
| int index = partName.lastIndexOf('.'); |
| //Dot at the beginning is not handled as an extension (hidden file) |
| if (index > 0 && index > partName.lastIndexOf('/')+1) { |
| partName = partName.substring (0, index); |
| } |
| } |
| if (dirSep!='/') { |
| partName = partName.replace('/',dirSep); |
| } |
| return partName; |
| } |
| |
| /** |
| * Finds a root in this ClassPath, that owns the given file. File resources, that |
| * are not reachable (they are hidden by other resources, or {@link FilteringPathResourceImplementation} excluded) are still considered |
| * to be part of the classpath and "owned" by one of its roots. |
| * <br> |
| * <b>Note:</b> This implementation assumes that the FileSystem hosting a classpath root |
| * contains the entire classpath subtree rooted at that root folder. |
| * @return classpath root, which hosts the specified resouce. It can return null, |
| * if the resource is not within the ClassPath contents. |
| * @param resource resource to find root for. |
| */ |
| public final @CheckForNull FileObject findOwnerRoot(FileObject resource) { |
| FileObject[] roots = getRoots(); |
| Set<FileObject> rootsSet = new HashSet<FileObject>(Arrays.asList(roots)); |
| for (FileObject f = resource; f != null; f = f.getParent()) { |
| if (rootsSet.contains(f)) { |
| return f; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Checks whether a FileObject lies on this classpath. |
| * {@link FilteringPathResourceImplementation} is considered. |
| * @return true, if the parameter is inside one of the classpath subtrees, |
| * false otherwise. |
| * @param f the FileObject to check |
| */ |
| public final boolean contains(FileObject f) { |
| FileObject root = findOwnerRoot(f); |
| if (root == null) { |
| return false; |
| } |
| FilteringPathResourceImplementation filter = root2Filter.get(root); |
| if (filter == null) { |
| return true; |
| } |
| String path = FileUtil.getRelativePath(root, f); |
| assert path != null : "could not find " + f + " in " + root; |
| if (f.isFolder()) { |
| path += "/"; // NOI18N |
| } |
| return filter.includes(root.toURL(), path); |
| } |
| |
| /** |
| * Determines if the resource is <i>visible</i> in the classpath, |
| * that is if the file will be reached when a process attempts to |
| * load a resource of that name. It will return false when the resource |
| * is not contained in the classpath, or the resource is {@link FilteringPathResourceImplementation excluded}. |
| * @param resource the resource whose visibility should be tested |
| * @return true, if the resource is contained in the classpath and visible; |
| * false otherwise. |
| */ |
| public final boolean isResourceVisible(FileObject resource) { |
| String resourceName = getResourceName(resource); |
| if (resourceName == null) |
| return false; |
| return findResource(resourceName) == resource; |
| } |
| |
| /** |
| * Adds a property change listener to the bean. |
| * @param l a listener to add |
| */ |
| public final synchronized void addPropertyChangeListener(PropertyChangeListener l) { |
| attachRootsListener (); |
| propSupport.addPropertyChangeListener(l); |
| } |
| |
| /** |
| * Removes the listener registered by {@link #addPropertyChangeListener(java.beans.PropertyChangeListener) }. |
| * @param l a listener to remove |
| */ |
| public final void removePropertyChangeListener(PropertyChangeListener l) { |
| propSupport.removePropertyChangeListener(l); |
| } |
| |
| /** |
| * Returns the {@link ClassPath}'s flags. |
| * @return the {@link Flag}s |
| * @since 1.44 |
| */ |
| @NonNull |
| public Set<Flag> getFlags() { |
| if (impl instanceof FlaggedClassPathImplementation) { |
| final Set<Flag> res = ((FlaggedClassPathImplementation)impl).getFlags(); |
| assert res != null : String.format( |
| "ClassPathImplementation %s : %s returned null flags.", //NOI18N |
| impl, |
| impl.getClass()); |
| return res; |
| } else { |
| return Collections.<Flag>emptySet(); |
| } |
| } |
| |
| /** |
| * Find the classpath of a given type, if any, defined for a given file. |
| * <p>This method may return null, if:</p> |
| * <ul> |
| * <li>the path type (<code>id</code> parameter) is not recognized |
| * <li>the path type is not defined for the given file object |
| * </ul> |
| * <p> |
| * Generally you may pass either an individual Java file, or the root of |
| * a Java package tree, interchangeably, since in most cases all files |
| * in a given tree will share a single classpath. |
| * </p> |
| * <p class="nonnormative"> |
| * Typically classpaths for files are defined by the owning project, but |
| * there may be other ways classpaths are defined. See {@link ClassPathProvider} |
| * for more details. |
| * </p> |
| * @param f the file, whose classpath settings should be returned (may <em>not</em> be null as of org.netbeans.api.java/1 1.4) |
| * @param id the type of the classpath (e.g. {@link #COMPILE}) |
| * @return classpath of the desired type for the given file object, or <code>null</code>, if |
| * there is no classpath available |
| * @see ClassPathProvider |
| */ |
| public static @CheckForNull ClassPath getClassPath(@NonNull FileObject f, @NonNull String id) { |
| if (f == null) { |
| // What else can we do?? Backwards compatibility only. |
| Thread.dumpStack(); |
| return null; |
| } |
| Lookup.Result<? extends ClassPathProvider> impls = implementations.get(); |
| if (impls == null) { |
| impls = Lookup.getDefault().lookupResult(ClassPathProvider.class); |
| if (!implementations.compareAndSet(null, impls)) { |
| impls = implementations.get(); |
| } |
| } |
| for (ClassPathProvider impl : impls.allInstances()) { |
| ClassPath cp = impl.findClassPath(f, id); |
| if (cp != null) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.log(Level.FINE, "getClassPath({0}, {1}) -> {2} from {3}", new Object[] {f, id, cp, impl}); |
| } |
| return cp; |
| } |
| } |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.log(Level.FINE, "getClassPath({0}, {1}) -> nil", new Object[] {f, id}); |
| } |
| return null; |
| } |
| |
| /** |
| * Fires a property change event on the specified property, notifying the |
| * old and new values. |
| * @param what name of the property |
| * @param oldV old value |
| * @param newV new value |
| */ |
| final void firePropertyChange(final String what, final Object oldV, final Object newV, final Object propagationId) { |
| final PropertyChangeEvent event = new PropertyChangeEvent (this, what, oldV, newV); |
| event.setPropagationId(propagationId); |
| propSupport.firePropertyChange(event); |
| } |
| |
| /** |
| * Policy for handling path items which cannot be converted into the desired format. |
| * @see #toString(ClassPath.PathConversionMode) |
| * @since org.netbeans.api.java/1 1.15 |
| */ |
| public enum PathConversionMode { |
| /** |
| * Drop entry silently. |
| */ |
| SKIP, |
| /** |
| * Entry is dropped but a warning is logged. |
| */ |
| WARN, |
| /** |
| * {@link #toString(ClassPath.PathConversionMode)} will fail with an {@link IllegalArgumentException}. |
| * Useful for unit tests. |
| */ |
| FAIL, |
| /** |
| * The entry is simply displayed as a URL. |
| * Useful for logging. |
| * @see ClassPath#toString() |
| */ |
| PRINT, |
| } |
| |
| /** |
| * Policy for handling in archive path. |
| * @see #toString(ClassPath.PathConversionMode, ClassPath.PathEmbeddingMode) |
| * @since 1.52 |
| */ |
| public enum PathEmbeddingMode { |
| /** |
| * The in archive path in included into the stringified classpath root. |
| * The embedded in archive path is separated by {@code !/} from the outer path. |
| */ |
| INCLUDE, |
| /** |
| * The in archive path in removed from the stringified classpath root. |
| */ |
| EXCLUDE, |
| /** |
| * The classpath root with in archive path is treated as invalid and handled |
| * according to {@link PathConversionMode}. |
| */ |
| FAIL |
| } |
| |
| /** |
| * ClassPath's flags. |
| * @since 1.44 |
| */ |
| public enum Flag { |
| /** |
| * The incomplete {@link ClassPath} is ignored by language features |
| * unless it's resolved and the {@link #INCOMPLETE} flag is removed. |
| */ |
| INCOMPLETE |
| } |
| |
| /** |
| * Render this classpath in the conventional format used by the Java launcher. |
| * @param conversionMode policy for converting unusual entries |
| * @param pathEmbeddingMode policy for handling in archive path |
| * @return a conventionally-formatted representation of the classpath |
| * @since 1.52 |
| * @see File#pathSeparator |
| * @see FileUtil#archiveOrDirForURL |
| * @see ClassPathSupport#createClassPath(String) |
| */ |
| @NonNull |
| @SuppressWarnings("fallthrough") |
| public String toString(@NonNull final PathConversionMode conversionMode, @NonNull final PathEmbeddingMode pathEmbeddingMode) { |
| StringBuilder b = new StringBuilder(); |
| for (Entry e : entries()) { |
| final URL u = e.getURL(); |
| String pathInArchive = ""; |
| boolean folder = false; |
| File f = FileUtil.archiveOrDirForURL(u); |
| if (f == null && FileUtil.isArchiveArtifact(u)) { |
| switch (pathEmbeddingMode) { |
| case EXCLUDE: |
| case INCLUDE: |
| final Object[] p = splitEmbedding(u); |
| if (p != null) { |
| f = FileUtil.archiveOrDirForURL((URL)p[0]); |
| if (pathEmbeddingMode == PathEmbeddingMode.INCLUDE) { |
| pathInArchive = (String) p[1]; |
| folder = (Boolean) p[2]; |
| } |
| } |
| case FAIL: |
| break; |
| default: |
| throw new IllegalArgumentException(String.valueOf(pathEmbeddingMode)); |
| } |
| } |
| if (f != null) { |
| if (b.length() > 0) { |
| b.append(File.pathSeparatorChar); |
| } |
| b.append(f.getAbsolutePath()); |
| if (!pathInArchive.isEmpty()) { |
| if (folder) { |
| b.append(File.separatorChar); |
| } |
| b.append(URL_EMBEDDING); |
| b.append(pathInArchive); |
| } |
| } else { |
| switch (conversionMode) { |
| case SKIP: |
| break; |
| case PRINT: |
| if (b.length() > 0) { |
| b.append(File.pathSeparatorChar); |
| } |
| b.append(u); |
| break; |
| case WARN: |
| LOG.log(Level.WARNING, "Encountered untranslatable classpath entry: {0}", u); |
| break; |
| case FAIL: |
| throw new IllegalArgumentException("Encountered untranslatable classpath entry: " + u); // NOI18N |
| default: |
| assert false : conversionMode; |
| } |
| } |
| } |
| return b.toString(); |
| } |
| |
| /** |
| * Render this classpath in the conventional format used by the Java launcher. |
| * @param conversionMode policy for converting unusual entries |
| * @return a conventionally-formatted representation of the classpath |
| * @since org.netbeans.api.java/1 1.15 |
| * @see File#pathSeparator |
| * @see FileUtil#archiveOrDirForURL |
| * @see ClassPathSupport#createClassPath(String) |
| */ |
| @NonNull |
| public String toString(@NonNull final PathConversionMode conversionMode) { |
| return toString(conversionMode, PathEmbeddingMode.FAIL); |
| } |
| |
| /** |
| * Calls {@link #toString(ClassPath.PathConversionMode)} with {@link ClassPath.PathConversionMode#PRINT}. |
| * @return a classpath suitable for logging or debugging |
| */ |
| @Override |
| @NonNull |
| public String toString() { |
| return toString(PathConversionMode.PRINT); |
| } |
| |
| @Override public boolean equals(Object obj) { |
| return obj instanceof ClassPath && impl.equals(((ClassPath) obj).impl); |
| } |
| |
| @Override public int hashCode() { |
| return impl.hashCode() ^ 22; |
| } |
| |
| /** |
| * Represents an individual entry in the ClassPath. An entry is a description |
| * of a folder, which is one of the ClassPath roots. Since the Entry does not |
| * control the folder's lifetime, the folder may be removed and the entry |
| * becomes invalid. It's also expected that ClassPath implementations may |
| * use other ClassPath entries as default or base for themselves, so Entries |
| * may be propagated between ClassPaths. |
| */ |
| public final class Entry { |
| |
| private final URL url; |
| private final AtomicReference<FileObject> root; |
| private volatile IOException lastError; |
| private FilteringPathResourceImplementation filter; |
| /*unit test*/ final AtomicReference<Boolean> isDataResult; |
| |
| /** |
| * Returns the ClassPath instance, which defines/introduces the Entry. |
| * Note that the return value may differ from the ClassPath instance, |
| * that produced this Entry from its <code>entries()</code> method. |
| * @return the ClassPath that defines the entry. |
| */ |
| public ClassPath getDefiningClassPath() { |
| return ClassPath.this; |
| } |
| |
| /** |
| * The method returns the root folder represented by the Entry. |
| * If the folder does not exist, or the folder is not readable, |
| * the method may return null. |
| * @return classpath entry root folder |
| */ |
| public FileObject getRoot() { |
| FileObject _root = root.get(); |
| if (_root != null && _root.isValid()) { |
| return _root; |
| } |
| for (int retryCount = 0; retryCount<=1; retryCount++) { //Bug 193086 : try to do refresh |
| FileObject newRoot = URLMapper.findFileObject(this.url); |
| _root = root.get(); |
| if (_root == null || !_root.isValid()) { |
| if (newRoot == null) { |
| this.lastError = new IOException(MessageFormat.format("The package root {0} does not exist or can not be read.", |
| new Object[] {this.url})); |
| return null; |
| } else if (isData(newRoot)) { |
| if (retryCount == 0) { |
| Logger l = Logger.getLogger("org.netbeans.modules.masterfs"); // NOI18N |
| Level prev = l.getLevel(); |
| try { |
| l.setLevel(Level.FINEST); |
| LOG.log(Level.WARNING, "Root is not folder {0}; about to refresh", newRoot); // NOI18N |
| newRoot.refresh(); |
| FileObject parent = newRoot.getParent(); |
| if (parent != null) { |
| LOG.log(Level.WARNING, "Refreshing its parent {0}", parent); // NOI18N |
| FileObject[] arr = parent.getChildren(); |
| parent.refresh(); |
| } |
| } finally { |
| l.setLevel(prev); |
| LOG.warning("End of refresh"); // NOI18N |
| } |
| continue; |
| } else { |
| String fileState = null; |
| try { |
| final File file = BaseUtilities.toFile(this.url.toURI()); |
| final boolean exists = file.exists(); |
| final boolean isDirectory = file.isDirectory(); |
| if (exists) { |
| if (isDirectory) { |
| fileState = "(exists: " + exists + //NOI18N |
| " file: " + file.isFile() + //NOI18N |
| " directory: "+ isDirectory + //NOI18N |
| " read: "+ file.canRead() + //NOI18N |
| " write: "+ file.canWrite()+ //NOI18N |
| " root: "+ _root + //NOI18N |
| " newRoot: "+ newRoot +")"; //NOI18N |
| } else { |
| LOG.log( |
| Level.WARNING, |
| "Ignoring non folder root : {0} on classpath ", //NOI18N |
| file); |
| return null; |
| } |
| } else { |
| if (newRoot.isValid()) { |
| LOG.log( |
| Level.WARNING, |
| "URL mapper returned a valid FileObject for non existent File : {0}, ignoring.", //NOI18N |
| file); |
| return null; |
| } else { |
| LOG.log( |
| Level.WARNING, |
| "URL mapper returned an invalid FileObject : {0}, ignoring.", //NOI18N |
| file); |
| return null; |
| } |
| } |
| } catch (IllegalArgumentException | URISyntaxException e) { |
| //Non local file - keep file null (not log file state) |
| } |
| throw new IllegalArgumentException ("Invalid ClassPath root: "+this.url+". The root must be a folder." + //NOI18N |
| (fileState != null ? fileState : "")); |
| } |
| } else { |
| if (!root.compareAndSet(_root, newRoot)) { |
| newRoot = root.get(); |
| } |
| return newRoot; |
| } |
| } else { |
| return _root; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @return true, iff the Entry refers to an existing and readable |
| * folder. |
| */ |
| public boolean isValid() { |
| FileObject r = getRoot(); |
| return r != null && r.isValid(); |
| } |
| |
| /** |
| * Retrieves the error condition of the Entry. The method will return |
| * null, if the <code>getRoot()</code> would return a FileObject. |
| * @return error condition for this Entry or null if the Entry is OK. |
| */ |
| public IOException getError() { |
| IOException error = this.lastError; |
| this.lastError = null; |
| return error; |
| } |
| |
| /** |
| * Returns URL of the class path root. |
| * This method is generally safer than {@link #getRoot} as |
| * it can be called even if the root does not currently exist. |
| * @return URL |
| * @since org.netbeans.api.java/1 1.4 |
| */ |
| public URL getURL () { |
| return this.url; |
| } |
| |
| /** |
| * Check whether a file is included in this entry. |
| * @param resource a path relative to @{link #getURL} (must be terminated with <em>/</em> if a non-root folder) |
| * @return true if it is {@link FilteringPathResourceImplementation#includes included} |
| * @since org.netbeans.api.java/1 1.13 |
| */ |
| public boolean includes(String resource) { |
| return filter == null || filter.includes(url, resource); |
| } |
| |
| /** |
| * Check whether a file is included in this entry. |
| * @param file a URL beneath @{link #getURL} |
| * @return true if it is {@link FilteringPathResourceImplementation#includes included} |
| * @throws IllegalArgumentException in case the argument is not beneath {@link #getURL} |
| * @since org.netbeans.api.java/1 1.13 |
| */ |
| public boolean includes(URL file) { |
| if (!file.toExternalForm().startsWith(url.toExternalForm())) { |
| throw new IllegalArgumentException(file + " not in " + url); |
| } |
| URI relative; |
| try { |
| relative = url.toURI().relativize(file.toURI()); |
| } catch (URISyntaxException x) { |
| throw new AssertionError(x); |
| } |
| assert !relative.isAbsolute() : "could not locate " + file + " in " + url; |
| return filter == null || filter.includes(url, relative.toString()); |
| } |
| |
| /** |
| * Check whether a file is included in this entry. |
| * @param file a file inside @{link #getRoot} |
| * @return true if it is {@link FilteringPathResourceImplementation#includes included} |
| * @throws IllegalArgumentException in case the argument is not beneath {@link #getRoot}, or {@link #getRoot} is null |
| * @since org.netbeans.api.java/1 1.13 |
| */ |
| public boolean includes(FileObject file) { |
| if (!file.isValid()) { |
| //Invalid FileObject is not included |
| return false; |
| } |
| FileObject r = getRoot(); |
| if (r == null) { |
| file.refresh(); |
| if (!file.isValid()) { |
| return false; |
| } else { |
| throw new IllegalArgumentException("no root in " + url); |
| } |
| } |
| String path = FileUtil.getRelativePath(r, file); |
| if (path == null) { |
| if (!file.isValid()) { |
| //Already tested above, but re-test if still valid |
| return false; |
| } |
| StringBuilder sb = new StringBuilder(); |
| sb.append(file).append(" (valid: ").append(file.isValid()).append(") not in "). // NOI18N |
| append(r).append(" (valid: ").append(r.isValid()).append(")"); // NOI18N |
| if (file.getPath().startsWith(r.getPath())) { |
| while (file.getPath().length() > r.getPath().length()) { |
| file = file.getParent(); |
| sb.append("\nChildren of ").append(file). |
| append(" (valid: ").append(file.isValid()).append(")"). |
| append(" are:\n ").append(Arrays.toString(file.getChildren())); |
| } |
| } else { |
| sb.append("\nRoot path is not prefix"); // NOI18N |
| } |
| throw new IllegalArgumentException(sb.toString()); |
| } |
| if (file.isFolder()) { |
| path += "/"; // NOI18N |
| } |
| return filter == null || filter.includes(url, path); |
| } |
| |
| Entry( |
| @NonNull final URL url, |
| @NullAllowed FilteringPathResourceImplementation filter) { |
| Parameters.notNull("url", url); //NOI18N |
| this.url = url; |
| this.filter = filter; |
| this.root = new AtomicReference<>(); |
| this.isDataResult = new AtomicReference<>(); |
| } |
| |
| public @Override String toString() { |
| return "Entry[" + url + "]"; // NOI18N |
| } |
| |
| @Override |
| public boolean equals (Object other) { |
| if (other instanceof ClassPath.Entry) { |
| return Objects.equals(((ClassPath.Entry)other).url, this.url); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode () { |
| return this.url == null ? 0 : this.url.hashCode(); |
| } |
| |
| private boolean isData(final FileObject fo) { |
| Boolean isd = isDataResult.getAndSet(null); |
| if (isd != null) { |
| return isd; |
| } else { |
| return fo.isData(); |
| } |
| } |
| } |
| |
| //-------------------- Implementation details ------------------------// |
| |
| private final PropertyChangeSupport propSupport; |
| |
| |
| /** |
| * Attaches the listener to the ClassPath.entries. |
| * Not synchronized, HAS TO be called from the synchronized block! |
| */ |
| private void attachRootsListener () { |
| assert Thread.holdsLock(this); |
| if (this.rootsListener == null) { |
| assert this.rootsCache == null; |
| this.rootsListener = new RootsListener (); |
| } |
| } |
| |
| /** |
| * Returns an array of pairs of strings, first string in the pair is the |
| * name, the next one is either the extension or null. |
| */ |
| private static String[] parseResourceName(String name) { |
| Collection<String> parsed = new ArrayList<String>(name.length() / 4); |
| char[] chars = name.toCharArray(); |
| char ch; |
| int pos = 0; |
| int dotPos = -1; |
| int startPos = 0; |
| |
| while (pos < chars.length) { |
| ch = chars[pos]; |
| switch (ch) { |
| case '.': |
| dotPos = pos; |
| break; |
| case '/': |
| // end of name component |
| if (dotPos != -1) { |
| parsed.add(name.substring(startPos, dotPos)); |
| parsed.add(name.substring(dotPos + 1, pos)); |
| } else { |
| parsed.add(name.substring(startPos, pos)); |
| parsed.add(null); |
| } |
| // reset variables: |
| startPos = pos + 1; |
| dotPos = -1; |
| break; |
| } |
| pos++; |
| } |
| // if the resource name ends with '/', just ignore the empty component |
| if (pos > startPos) { |
| if (dotPos != -1) { |
| parsed.add(name.substring(startPos, dotPos)); |
| parsed.add(name.substring(dotPos + 1, pos)); |
| } else { |
| parsed.add(name.substring(startPos, pos)); |
| parsed.add(null); |
| } |
| } |
| if ((parsed.size() % 2) != 0) { |
| System.err.println("parsed size is not even!!"); |
| System.err.println("input = " + name); |
| } |
| return parsed.toArray(new String[parsed.size()]); |
| } |
| |
| /** |
| * Finds a path underneath the `parent'. Name parts is an array of string pairs, |
| * the first String in a pair is the basename, the second is the extension or null |
| * for no extension. |
| */ |
| private static FileObject findPath( |
| @NonNull FileObject parent, |
| @NonNull final String[] nameParts, |
| @NonNull /*out*/ final String[] relativePath) { |
| assert relativePath.length == 1; |
| final StringBuilder relativePathBuilder = new StringBuilder(); |
| FileObject child; |
| String separator = ""; //NOI18N |
| for (int i = 0; i < nameParts.length && parent != null; i += 2, parent = child) { |
| child = parent.getFileObject(nameParts[i], nameParts[i + 1]); |
| if (child != null) { |
| relativePathBuilder.append(separator).append(child.getNameExt()); |
| } |
| separator = "/"; //NOI18N |
| } |
| if (parent != null) { |
| if (parent.isFolder()) { |
| relativePathBuilder.append(separator); |
| } |
| relativePath[0] = relativePathBuilder.toString(); |
| } else { |
| relativePath[0] = null; |
| } |
| return parent; |
| } |
| |
| /** |
| * Searches for a resource in one or more roots, gives back the index of |
| * the first untouched root. |
| */ |
| private FileObject findResourceImpl(FileObject[] roots, |
| int[] rootIndex, String[] nameComponents) { |
| int ridx; |
| FileObject f = null; |
| final String[] pathOut = new String[1]; |
| for (ridx = rootIndex[0]; ridx < roots.length && f == null; ridx++) { |
| f = findPath(roots[ridx], nameComponents, pathOut); |
| FilteringPathResourceImplementation filter = root2Filter.get(roots[ridx]); |
| if (filter != null) { |
| if (f != null) { |
| if (!filter.includes(roots[ridx].toURL(), pathOut[0].toString())) { |
| f = null; |
| } |
| } |
| } |
| } |
| rootIndex[0] = ridx; |
| return f; |
| } |
| |
| @CheckForNull |
| private Object[] splitEmbedding(@NonNull final URL url) { |
| final String surl = url.toExternalForm(); |
| final int index = surl.lastIndexOf(URL_EMBEDDING); |
| final String archiveRoot; |
| final String pathInArchive; |
| final boolean folder; |
| if (index >= 0) { |
| archiveRoot = surl.substring(0, index+URL_EMBEDDING.length()); |
| pathInArchive = surl.substring(index+URL_EMBEDDING.length()); |
| folder = index > 0 && surl.charAt(index-1) == '/'; //NOI18N |
| } else { |
| archiveRoot = surl; |
| pathInArchive = ""; //NOI18N |
| folder = false; |
| } |
| try { |
| return new Object[] { |
| new URL(archiveRoot), |
| pathInArchive, |
| folder}; |
| } catch (MalformedURLException e) { |
| LOG.log( |
| Level.WARNING, |
| "Invalid URL: {0} ({1})", //NOI18N |
| new Object[]{ |
| archiveRoot, |
| e.getMessage() |
| }); |
| return null; |
| } |
| } |
| |
| private static final Reference<ClassLoader> EMPTY_REF = new SoftReference<ClassLoader>(null); |
| |
| private Reference<ClassLoader> refClassLoader = EMPTY_REF; |
| |
| /* package private */synchronized void resetClassLoader(ClassLoader cl) { |
| if (refClassLoader.get() == cl) |
| refClassLoader = EMPTY_REF; |
| } |
| |
| /** |
| * Returns a ClassLoader for loading classes from this ClassPath. |
| * <p> |
| * If <code>cache</code> is false, then |
| * the method will always return a new class loader. If that parameter is true, |
| * the method may return a loader which survived from a previous call to the same <code>ClassPath</code>. |
| * |
| * @param cache true if it is permissible to cache class loaders between calls |
| * @return class loader which uses the roots in this class path to search for classes and resources |
| * @since 1.2.1 |
| */ |
| public final synchronized ClassLoader getClassLoader(boolean cache) { |
| // XXX consider adding ClassLoader and/or InputOutput and/or PermissionCollection params |
| ClassLoader o = refClassLoader.get(); |
| if (!cache || o == null) { |
| o = ClassLoaderSupport.create(this); |
| refClassLoader = new SoftReference<ClassLoader>(o); |
| } |
| return o; |
| } |
| |
| |
| private class SPIListener implements PropertyChangeListener { |
| private Object propIncludesPropagationId; |
| public void propertyChange(PropertyChangeEvent evt) { |
| String prop = evt.getPropertyName(); |
| if (ClassPathImplementation.PROP_RESOURCES.equals(prop) || PathResourceImplementation.PROP_ROOTS.equals(prop)) { |
| synchronized (ClassPath.this) { |
| if (rootsListener != null) { |
| rootsListener.removeAllRoots (); |
| } |
| entriesCache = null; |
| rootsCache = null; |
| invalidEntries++; |
| invalidRoots++; |
| } |
| firePropertyChange (PROP_ENTRIES,null,null,null); |
| firePropertyChange (PROP_ROOTS,null,null,null); |
| } else if (FilteringPathResourceImplementation.PROP_INCLUDES.equals(prop)) { |
| boolean fire; |
| synchronized (this) { |
| Object id = evt.getPropagationId(); |
| fire = propIncludesPropagationId == null || !propIncludesPropagationId.equals(id); |
| propIncludesPropagationId = id; |
| } |
| if (fire) { |
| firePropertyChange(PROP_INCLUDES, null, null, evt.getPropagationId()); |
| } |
| } else if (FlaggedClassPathImplementation.PROP_FLAGS.equals(prop)) { |
| firePropertyChange(PROP_FLAGS, null, null, null); |
| } |
| if (ClassPathImplementation.PROP_RESOURCES.equals(prop)) { |
| final List<? extends PathResourceImplementation> resources = impl.getResources(); |
| if (resources == null) { |
| LOG.log(Level.WARNING, "ClassPathImplementation.getResources cannot return null; impl class: {0}", impl.getClass().getName()); |
| return; |
| } |
| synchronized (ClassPath.this) { |
| for (final Iterator<Object[]> it = weakPListeners.iterator(); it.hasNext();) { |
| final Object[] rwp = it.next(); |
| it.remove(); |
| ((PathResourceImplementation)rwp[0]).removePropertyChangeListener((PropertyChangeListener)rwp[1]); |
| } |
| assert weakPListeners.isEmpty(); |
| for (PathResourceImplementation pri : resources) { |
| final PropertyChangeListener weakPListener = WeakListeners.propertyChange(pListener, pri); |
| pri.addPropertyChangeListener(weakPListener); |
| weakPListeners.add(new Object[]{pri, weakPListener}); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| private synchronized RootsListener getRootsListener () { |
| return this.rootsListener; |
| } |
| |
| |
| private class RootsListener extends FileChangeAdapter { |
| |
| private final Set</*@GuardedBy("this")*/File> roots; |
| |
| private RootsListener () { |
| roots = new HashSet<> (); |
| } |
| |
| public void addRoots (final Set<? extends File> newRoots) { |
| Parameters.notNull("urls",newRoots); //NOI18N |
| synchronized (this) { |
| final Set<File> toRemove = new HashSet<>(roots); |
| toRemove.removeAll(newRoots); |
| final Set<? extends File> toAdd = new HashSet<>(newRoots); |
| toAdd.removeAll(roots); |
| for (File root : toRemove) { |
| safeRemoveFileChangeListener(root); |
| roots.remove(root); |
| } |
| for (File root : toAdd) { |
| safeAddFileChangeListener(root); |
| roots.add (root); |
| } |
| } |
| } |
| |
| |
| public synchronized void removeAllRoots () { |
| for (final Iterator<File> it = roots.iterator(); it.hasNext();) { |
| final File root = it.next(); |
| it.remove(); |
| safeRemoveFileChangeListener(root); |
| } |
| } |
| |
| @Override |
| public void fileFolderCreated(FileEvent fe) { |
| this.processEvent (fe); |
| } |
| |
| @Override |
| public void fileDataCreated(FileEvent fe) { |
| this.processEvent (fe); |
| } |
| |
| @Override |
| public void fileChanged(FileEvent fe) { |
| processEvent(fe); |
| } |
| |
| @Override |
| public void fileDeleted(FileEvent fe) { |
| this.processEvent (fe); |
| } |
| |
| @Override |
| public void fileRenamed(FileRenameEvent fe) { |
| this.processEvent (fe); |
| } |
| |
| private void processEvent (FileEvent fe) { |
| synchronized (ClassPath.this) { |
| ClassPath.this.rootsCache = null; |
| ClassPath.this.invalidRoots++; |
| } |
| ClassPath.this.firePropertyChange(PROP_ROOTS,null,null,null); |
| } |
| |
| private void safeRemoveFileChangeListener(@NonNull final File file) { |
| try { |
| FileUtil.removeFileChangeListener(this, file); |
| } catch (IllegalArgumentException iae) { |
| LOG.log(Level.FINE, iae.getMessage()); |
| } |
| } |
| |
| private void safeAddFileChangeListener(@NonNull final File file) { |
| try { |
| FileUtil.addFileChangeListener(this, file); |
| } catch (IllegalArgumentException iae) { |
| LOG.log(Level.FINE, iae.getMessage()); |
| } |
| } |
| } |
| } |