Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.netbeans.api.java.classpath.ClassPath Maven / Gradle / Ivy
/*
* 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 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 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 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.
* 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 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 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 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 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 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 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());
}
}
}
}