Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion java/java.freeform/nbproject/project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# under the License.

javac.compilerargs=-Xlint -Xlint:-serial
javac.release=17
javac.release=21
javadoc.arch=${basedir}/arch.xml
javadoc.apichanges=${basedir}/apichanges.xml

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.classpath.GlobalPathRegistry;
Expand Down Expand Up @@ -70,9 +71,13 @@
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.netbeans.spi.project.support.ant.PropertyUtils;
import org.openide.ErrorManager;
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.util.Mutex;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import org.openide.xml.XMLUtil;
Expand Down Expand Up @@ -104,6 +109,11 @@ final class Classpaths implements ClassPathProvider, AntProjectListener, Propert

private static final ErrorManager err = ErrorManager.getDefault().getInstance(Classpaths.class.getName());

/// Recomputes wildcard classpaths off the file-event thread; see `wildcardListener`.
private static final RequestProcessor WILDCARD_RP = new RequestProcessor(Classpaths.class.getName() + ".wildcard", 1); // NOI18N
/// Coalescing delay (ms) so that a build creating many JARs triggers a single refresh.
private static final int WILDCARD_REFRESH_DELAY = 500;

//for tests only:
static CountDownLatch TESTING_LATCH = null;

Expand Down Expand Up @@ -403,22 +413,33 @@ private List<URL> createSourcePath(List<String> packageRootNames) {
return roots;
}

private List<URL> createCompileClasspath(Element compilationUnitEl) {
private List<URL> createCompileClasspath(Element compilationUnitEl, Set<File> watchedDirs) {
for (Element e : XMLUtil.findSubElements(compilationUnitEl)) {
if (e.getLocalName().equals("classpath") && e.getAttribute("mode").equals("compile")) { // NOI18N
return createClasspath(e, new RemoveSources(helper, sfbqImpl));
return createClasspath(e, new RemoveSources(helper, sfbqImpl), watchedDirs);
}
}
// None specified; assume it is empty.
return Collections.emptyList();
}

/**
* Create a classpath from a &lt;classpath&gt; element.
* <p>
* A path entry whose last path component contains a wildcard (<code>*</code>
* or <code>?</code>) is expanded to the archives in the preceding directory
* whose names match the glob; e.g. <code>build/lib/*</code> or
* <code>build/lib/*.jar</code> pick up every JAR in <code>build/lib</code>.
* This mirrors the wildcard classpath syntax understood by the {@code java}
* launcher and lets freeform projects reference a directory of libraries
* without listing each JAR. The directories backing any wildcards are added
* to {@code watchedDirs} so the caller can recompute when their contents
* change (e.g. after a build produces new JARs).
*/
private List<URL> createClasspath(
final Element classpathEl,
final Function<URL,Collection<URL>> translate) {
final Function<URL,Collection<URL>> translate,
final Set<File> watchedDirs) {
String cp = XMLUtil.findText(classpathEl);
if (cp == null) {
cp = "";
Expand All @@ -430,26 +451,92 @@ private List<URL> createClasspath(
final String[] path = PropertyUtils.tokenizePath(cpEval);
final List<URL> res = new ArrayList<>();
for (String pathElement : path) {
res.addAll(translate.apply(createClasspathEntry(pathElement)));
for (URL entry : createClasspathEntries(pathElement, watchedDirs)) {
res.addAll(translate.apply(entry));
}
}
return res;
}


/**
* Turn a single (already property-evaluated) classpath token into zero or
* more classpath root URLs. Ordinary tokens map to exactly one URL; a token
* whose last path component is a filename glob is expanded to the matching
* archives in the directory it names.
*/
private List<URL> createClasspathEntries(String text, Set<File> watchedDirs) {
final int slash = Math.max(text.lastIndexOf('/'), text.lastIndexOf(File.separatorChar));
final String lastComponent = slash >= 0 ? text.substring(slash + 1) : text;
if (lastComponent.indexOf('*') < 0 && lastComponent.indexOf('?') < 0) {
return Collections.singletonList(createClasspathEntry(text));
}
final String prefix = slash >= 0 ? text.substring(0, slash) : ""; // NOI18N
final File dir = helper.resolveFile(prefix.isEmpty() ? "." : prefix); // NOI18N
if (watchedDirs != null) {
// Watch the directory even if it does not exist yet: a build may
// create it (and the matching JARs) after the project is opened.
watchedDirs.add(dir);
}
final File[] kids = dir.listFiles();
if (kids == null) {
return Collections.emptyList();
}
// Sort for a stable classpath order independent of directory listing order.
Arrays.sort(kids);
final Pattern pattern = wildcardToRegex(lastComponent);
final List<URL> res = new ArrayList<>();
for (File kid : kids) {
if (!kid.isFile() || !pattern.matcher(kid.getName()).matches()) {
continue;
}
// urlForArchiveOrDir returns null for an existing file that is not a
// valid archive, so this keeps only archives (matches the java
// launcher's JARs-only rule for a dir/* entry).
final URL entry = FileUtil.urlForArchiveOrDir(kid);
if (entry != null) {
res.add(entry);
}
}
return res;
}

/**
* Translate a filename glob (<code>*</code> matches any run of characters,
* <code>?</code> matches a single character) into a case-insensitive regex.
*/
private static Pattern wildcardToRegex(String glob) {
final StringBuilder sb = new StringBuilder(glob.length() + 8);
for (int i = 0; i < glob.length(); i++) {
final char c = glob.charAt(i);
switch (c) {
case '*' -> sb.append(".*"); // NOI18N
case '?' -> sb.append('.');
default -> {
if ("\\.[]{}()+-^$|".indexOf(c) >= 0) { // NOI18N
sb.append('\\');
}
sb.append(c);
}
}
Comment on lines +511 to +520

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want you could set javac.release=21 and use the arrow syntax.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in c9969aa

}
return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE);
}

private URL createClasspathEntry(String text) {
File entryFile = helper.resolveFile(text);
return FileUtil.urlForArchiveOrDir(entryFile);
}
private List<URL> createExecuteClasspath(List<String> packageRoots, Element compilationUnitEl) {

private List<URL> createExecuteClasspath(List<String> packageRoots, Element compilationUnitEl, Set<File> watchedDirs) {
for (Element e : XMLUtil.findSubElements(compilationUnitEl)) {
if (e.getLocalName().equals("classpath") && e.getAttribute("mode").equals("execute")) { // NOI18N
return createClasspath(e, new RemoveSources(helper, sfbqImpl));
return createClasspath(e, new RemoveSources(helper, sfbqImpl), watchedDirs);
}
}
// None specified; assume it is same as compile classpath plus (cf. #49113) <built-to> dirs/JARs
// if there are any (else include the source dir(s) as a fallback for the I18N wizard to work).
Set<URL> urls = new LinkedHashSet<>();
urls.addAll(createCompileClasspath(compilationUnitEl));
urls.addAll(createCompileClasspath(compilationUnitEl, watchedDirs));
final Project prj = FileOwnerQuery.getOwner(helper.getProjectDirectory());
if (prj != null) {
for (URL src : createSourcePath(packageRoots)) {
Expand All @@ -459,27 +546,27 @@ private List<URL> createExecuteClasspath(List<String> packageRoots, Element comp
return new ArrayList<>(urls);
}

private List<URL> createProcessorClasspath(Element compilationUnitEl) {
private List<URL> createProcessorClasspath(Element compilationUnitEl, Set<File> watchedDirs) {
final Element ap = XMLUtil.findElement(compilationUnitEl, AnnotationProcessingQueryImpl.EL_ANNOTATION_PROCESSING, JavaProjectNature.NS_JAVA_LASTEST);
if (ap != null) {
final Element path = XMLUtil.findElement(ap, AnnotationProcessingQueryImpl.EL_PROCESSOR_PATH, JavaProjectNature.NS_JAVA_LASTEST);
if (path != null) {
return createClasspath(path, new RemoveSources(helper, sfbqImpl));
return createClasspath(path, new RemoveSources(helper, sfbqImpl), watchedDirs);
}
}
// None specified; assume it is the same as the compile classpath.
return createCompileClasspath(compilationUnitEl);
return createCompileClasspath(compilationUnitEl, watchedDirs);
}

private List<URL> createBootClasspath(Element compilationUnitEl) {
private List<URL> createBootClasspath(Element compilationUnitEl, Set<File> watchedDirs) {
for (Element e : XMLUtil.findSubElements(compilationUnitEl)) {
if (e.getLocalName().equals("classpath") && e.getAttribute("mode").equals("boot")) { // NOI18N
return createClasspath(e, new Function<URL,Collection<URL>>() {
@Override
public Collection<URL> apply(URL p) {
return Collections.singleton(p);
}
});
}, watchedDirs);
}
}
// None specified;
Expand Down Expand Up @@ -540,7 +627,25 @@ private final class MutableClassPathImplementation implements ClassPathImplement
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private List<URL> roots; // should always be non-null
private List<PathResourceImplementation> resources;

/** Directories backing wildcard classpath entries we currently listen to. */
private final Set<File> watchedWildcardDirs = new HashSet<File>();
/**
* Coalesces the refresh triggered by wildcard-directory changes and, just
* as importantly, moves it off the file-event thread: {@link #syncWildcardListeners}
* registers listeners while holding this object's monitor, so refreshing
* synchronously from an event could invert lock order with that registration.
*/
private final RequestProcessor.Task wildcardRefreshTask = WILDCARD_RP.create(new Runnable() {
public @Override void run() { pathsChanged(); }
});
/** Refreshes this path when the contents of a watched wildcard directory change. */
private final FileChangeAdapter wildcardListener = new FileChangeAdapter() {
public @Override void fileDataCreated(FileEvent fe) { wildcardRefreshTask.schedule(WILDCARD_REFRESH_DELAY); }
public @Override void fileFolderCreated(FileEvent fe) { wildcardRefreshTask.schedule(WILDCARD_REFRESH_DELAY); }
public @Override void fileDeleted(FileEvent fe) { wildcardRefreshTask.schedule(WILDCARD_REFRESH_DELAY); }
public @Override void fileRenamed(FileRenameEvent fe) { wildcardRefreshTask.schedule(WILDCARD_REFRESH_DELAY); }
};

public MutableClassPathImplementation(List<String> packageRootNames, String type, Element initialCompilationUnit) {
this.packageRootNames = packageRootNames;
this.type = type;
Expand Down Expand Up @@ -569,24 +674,28 @@ private Element findCompilationUnit() {
*/
private boolean initRoots(Element compilationUnitEl) {
List<URL> oldRoots = roots;
// Directories backing any wildcard entries encountered while (re)computing
// the roots; SOURCE paths never use wildcards so the set stays empty there.
Set<File> watchedDirs = new HashSet<File>();
if (compilationUnitEl != null) {
if (type.equals(ClassPath.SOURCE)) {
roots = createSourcePath(packageRootNames);
} else if (type.equals(ClassPath.COMPILE)) {
roots = createCompileClasspath(compilationUnitEl);
roots = createCompileClasspath(compilationUnitEl, watchedDirs);
} else if (type.equals(ClassPath.EXECUTE)) {
roots = createExecuteClasspath(packageRootNames, compilationUnitEl);
roots = createExecuteClasspath(packageRootNames, compilationUnitEl, watchedDirs);
} else if (type.equals(JavaClassPathConstants.PROCESSOR_PATH)) {
roots = createProcessorClasspath(compilationUnitEl);
roots = createProcessorClasspath(compilationUnitEl, watchedDirs);
} else {
assert type.equals(ClassPath.BOOT) : type;
roots = createBootClasspath(compilationUnitEl);
roots = createBootClasspath(compilationUnitEl, watchedDirs);
}
} else {
// Dead.
roots = Collections.emptyList();
}
assert roots != null;
syncWildcardListeners(watchedDirs);
if (!roots.equals(oldRoots)) {
resources = new ArrayList<PathResourceImplementation>(roots.size());
for (URL root : roots) {
Expand All @@ -607,6 +716,26 @@ private boolean initRoots(Element compilationUnitEl) {
}
}

/**
* Register file listeners on exactly the set of directories backing the
* current wildcard entries, so newly built (or removed) JARs refresh the
* path. Listeners for directories no longer referenced are dropped.
*/
private void syncWildcardListeners(Set<File> newDirs) {
for (Iterator<File> it = watchedWildcardDirs.iterator(); it.hasNext(); ) {
File dir = it.next();
if (!newDirs.contains(dir)) {
FileUtil.removeFileChangeListener(wildcardListener, dir);
it.remove();
}
}
for (File dir : newDirs) {
if (watchedWildcardDirs.add(dir)) {
FileUtil.addFileChangeListener(wildcardListener, dir);
}
}
}

public List<PathResourceImplementation> getResources() {
assert resources != null;
return resources;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ Cf. http://projects.netbeans.org/buildsys/design.html#freeform
<xsd:element name="unit-tests" minOccurs="0"><xsd:complexType/></xsd:element>
<xsd:element name="classpath" minOccurs="0" maxOccurs="unbounded">
<!-- XXX use schema to declare that the mode must be unique within this group -->
<xsd:annotation>
<xsd:documentation>
A path (elements separated by ':' or ';') of directories and archives.
A path element whose last component is a filename glob (containing
'*' or '?') is expanded to the archives in the preceding directory
whose names match, e.g. "build/lib/*" or "build/lib/*.jar" reference
every JAR in "build/lib". This mirrors the wildcard classpath syntax
of the java launcher and avoids having to list each JAR; the matching
directory is watched so newly built JARs are picked up automatically.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:simpleContent>
<xsd:extension base="substitutable-text">
Expand Down
Loading