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

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

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.api.java.classpath;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.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}.
 * 
* 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 reachable (visible to a ClassLoader). One can translate * filenames to resource names and vice versa. *

* A client may obtain a ClassPath instance using * ClassPath.getClassPath(id) 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. *

* It corresponds to the -classpath option to java * (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 * URLClassLoader (plus URLs present in parent class loaders * but excluding the bootstrap and extension class loaders). *

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

* It corresponds to the -classpath option to javac: * i.e. already-compiled classes which some new sources need to compile against, * besides what is already in the JRE. *

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

* It is similar to the -sourcepath option of javac. *

*

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

*

* Note that each source file for which editor code completion (and similar * actions) should work should have a classpath of this type. *

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

* It corresponds to the -Xbootclasspath and -Xext * options to java (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 plus 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. *

* @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> 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 root2Filter = new WeakHashMap(); private PropertyChangeListener pListener; private final List weakPListeners = new LinkedList(); //todo: Replace with Pair when Pair available private RootsListener rootsListener; private List 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 entries = this.entries(); final List>> 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>> createRoots (final List entries) { final List>> 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>> roots) { final Set listenOn = new HashSet<>(); for (Pair> 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 entries() { long current; synchronized (this) { if (this.entriesCache != null) { return this.entriesCache; } current = this.invalidEntries; } List resources = impl.getResources(); if (resources == null) throw new NullPointerException ( "ClassPathImplementation.getResources() returned null. ClassPathImplementation.class: " + impl.getClass ().toString () + " ClassPathImplementation: " + impl.toString () ); final List snapshot = new ArrayList(); for (PathResourceImplementation pr : resources) { snapshot.add(new Object[] {pr, pr.getRoots()}); } List 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 createEntries (final List resources) { List cache = new ArrayList (); for (final Iterator 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 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.
* If the resourceName identifies a package, this method will * return the FileObject for the first package fragment * in the ClassPath. * {@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 findAllResources(String resourceName) { FileObject[] roots = getRoots(); List l = new ArrayList(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 null if the fileobject * is not underneath any of classpath roots.
* 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. *
* Note: 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 rootsSet = new HashSet(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 visible 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 getFlags() { if (impl instanceof FlaggedClassPathImplementation) { final Set res = ((FlaggedClassPathImplementation)impl).getFlags(); assert res != null : String.format( "ClassPathImplementation %s : %s returned null flags.", //NOI18N impl, impl.getClass()); return res; } else { return Collections.emptySet(); } } /** * Find the classpath of a given type, if any, defined for a given file. *

This method may return null, if:

*
    *
  • the path type (id parameter) is not recognized *
  • the path type is not defined for the given file object *
*

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

*

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

* @param f the file, whose classpath settings should be returned (may not 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 null, 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 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 root; private volatile IOException lastError; private FilteringPathResourceImplementation filter; /*unit test*/ final AtomicReference 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 entries() 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 getRoot() 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 / 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 parsed = new ArrayList(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[0]); } /** * 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 EMPTY_REF = new SoftReference(null); private Reference 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. *

* If cache 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 ClassPath. * * @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(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 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 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 roots; private RootsListener () { roots = new HashSet<> (); } public void addRoots (final Set newRoots) { Parameters.notNull("urls",newRoots); //NOI18N synchronized (this) { final Set toRemove = new HashSet<>(roots); toRemove.removeAll(newRoots); final Set 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 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()); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy