Skip to content

Commit edfc4a3

Browse files
committed
Load BEAUti templates from the class path, not only module layers
BeautiDoc discovered fxtemplates only inside resolved module layers: processTemplate -> loadResourceFromModules scans ModuleLayer.boot() plus plugin layers, and getTemplateSources enumerates them via BEASTClassLoader.listResources, which does the same. That is correct for the installed launcher and for 'mvn exec -m beast.fx/...Beauti', where the beast modules are on the module path and resolved. But when BEAUti's dependencies are on the class path instead -- embedded in another tool, or under a test runner where everything loads in the unnamed module -- no beast.fx module is resolved, so the scans find nothing and template loading fails with 'Cannot find template: fxtemplates/Standard.xml', even though the template is right there in the beast-fx jar. Add a class-path fallback in both places: - loadResourceFromModules: after the module scan, try the context class loader's getResourceAsStream on the namespaced name (beast.fx/fxtemplates/Standard.xml) and then the bare name. - getTemplateSources: after the module scan, enumerate fxtemplates/*.xml by scanning java.class.path entries (jars and directories) and read each from its resource URL. Both are additive and last-precedence: in a module-path launch the module scan still finds everything first and these fallbacks contribute only duplicates, which are dropped by name. The content is still read from the jar, never copied out. beast-fx BeautiSimpleTest still passes unchanged.
1 parent c1138f3 commit edfc4a3

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

beast-fx/src/main/java/beastfx/app/inputeditor/BeautiDoc.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
import java.lang.module.ModuleReader;
2323
import java.lang.module.ResolvedModule;
2424
import java.lang.reflect.InvocationTargetException;
25+
import java.net.URL;
2526
import java.net.URLDecoder;
2627
import java.nio.charset.StandardCharsets;
28+
import java.util.zip.ZipEntry;
29+
import java.util.zip.ZipFile;
2730
import java.util.ArrayList;
31+
import java.util.Enumeration;
2832
import java.util.Arrays;
2933
import java.util.Collection;
3034
import java.util.HashMap;
@@ -790,6 +794,97 @@ private static class ModuleTemplateSource extends TemplateSource {
790794
@Override String describe() { return "module resource " + resource.path + " in " + resource.moduleName; }
791795
}
792796

797+
/** A template found by scanning the class path (see {@link #classpathTemplateSources}).
798+
* Used when BEAUti runs with its dependencies on the class path rather than the
799+
* module path -- e.g. embedded in another tool or under a test runner -- where the
800+
* module-layer scan in {@link BEASTClassLoader#listResources} finds nothing. The
801+
* content is still read from the jar (or classes dir) via the resource URL. */
802+
private static class UrlTemplateSource extends TemplateSource {
803+
final URL url;
804+
805+
UrlTemplateSource(String fileName, URL url) {
806+
super(fileName);
807+
this.url = url;
808+
}
809+
810+
@Override String readXML() throws IOException {
811+
try (InputStream is = url.openStream()) {
812+
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
813+
}
814+
}
815+
@Override String loadName() { return fileName; }
816+
@Override String describe() { return "class-path resource " + url; }
817+
}
818+
819+
/** Enumerate {@code fxtemplates/*.xml} on the class path by scanning the entries of
820+
* {@code java.class.path} (jars and directories). This is the class-path counterpart
821+
* to {@link BEASTClassLoader#listResources}, which only sees resources inside
822+
* resolved module layers: when BEAUti is launched on the module path (the installed
823+
* launcher, {@code -m beast.fx/...}) that scan already finds every template and this
824+
* method's results are dropped as duplicates by name; it only contributes when the
825+
* dependencies are on the class path instead.
826+
*
827+
* <p>Templates are namespaced by module, e.g. {@code beast.fx/fxtemplates/Standard.xml}
828+
* or {@code bmodeltest/fxtemplates/bModelTest.xml}, so we match any path ending in
829+
* {@code fxtemplates/<name>.xml}. Reading a fragment of the class path this way does
830+
* not follow a manifest-only {@code Class-Path} (as some launchers use for very long
831+
* class paths); in that case the module-layer scan is expected to have covered it. */
832+
private static List<TemplateSource> classpathTemplateSources() {
833+
List<TemplateSource> sources = new ArrayList<>();
834+
String classpath = System.getProperty("java.class.path");
835+
if (classpath == null || classpath.isEmpty()) {
836+
return sources;
837+
}
838+
String needle = "/" + BeautiConfig.TEMPLATE_DIR + "/"; // "/fxtemplates/"
839+
for (String entry : classpath.split(System.getProperty("path.separator"))) {
840+
File f = new File(entry);
841+
if (!f.exists()) {
842+
continue;
843+
}
844+
try {
845+
if (f.isDirectory()) {
846+
collectTemplateFilesInDir(new File(f, BeautiConfig.TEMPLATE_DIR), sources);
847+
// module-namespaced layout: <dir>/<module>/fxtemplates/*.xml
848+
File[] subdirs = f.listFiles(File::isDirectory);
849+
if (subdirs != null) {
850+
for (File sub : subdirs) {
851+
collectTemplateFilesInDir(new File(sub, BeautiConfig.TEMPLATE_DIR), sources);
852+
}
853+
}
854+
} else if (entry.toLowerCase().endsWith(".jar")) {
855+
try (ZipFile zip = new ZipFile(f)) {
856+
Enumeration<? extends ZipEntry> en = zip.entries();
857+
while (en.hasMoreElements()) {
858+
String name = en.nextElement().getName();
859+
if (name.toLowerCase().endsWith(".xml")
860+
&& (name.contains(needle) || name.startsWith(BeautiConfig.TEMPLATE_DIR + "/"))) {
861+
String base = name.substring(name.lastIndexOf('/') + 1);
862+
URL url = new URL("jar:" + f.toURI().toURL() + "!/" + name);
863+
sources.add(new UrlTemplateSource(base, url));
864+
}
865+
}
866+
}
867+
}
868+
} catch (IOException e) {
869+
// skip unreadable class-path entry
870+
}
871+
}
872+
return sources;
873+
}
874+
875+
/** Add every {@code *.xml} directly inside {@code dir} as a template source. */
876+
private static void collectTemplateFilesInDir(File dir, List<TemplateSource> sources) throws IOException {
877+
File[] files = dir.listFiles();
878+
if (files == null) {
879+
return;
880+
}
881+
for (File f : files) {
882+
if (f.isFile() && f.getName().toLowerCase().endsWith(".xml")) {
883+
sources.add(new UrlTemplateSource(f.getName(), f.toURI().toURL()));
884+
}
885+
}
886+
}
887+
793888
/** Candidate directories that may hold an {@code fxtemplates/} folder: the
794889
* classpath-derived directories (covering IDE/dev and jpackage layouts) plus
795890
* the installed BEAST package directories. Insertion order is preserved so
@@ -862,6 +957,14 @@ private static List<TemplateSource> getTemplateSources() {
862957
byName.put(base, new ModuleTemplateSource(base, res));
863958
}
864959
}
960+
// 3. class path: covers launches where dependencies are on the class path rather
961+
// than in a module layer, which (1) and (2) miss. Added last so filesystem and
962+
// module entries keep precedence when the same template is visible both ways.
963+
for (TemplateSource src : classpathTemplateSources()) {
964+
if (!byName.containsKey(src.fileName)) {
965+
byName.put(src.fileName, src);
966+
}
967+
}
865968
return new ArrayList<>(byName.values());
866969
}
867970

@@ -933,6 +1036,23 @@ private static String loadResourceFromModules(String resourcePath) {
9331036
}
9341037
}
9351038
}
1039+
// Class-path fallback: when BEAUti's dependencies are on the class path rather
1040+
// than in a module layer, the scan above finds nothing. The template still lives
1041+
// in the jar and is reachable by its namespaced resource name (e.g.
1042+
// beast.fx/fxtemplates/Standard.xml), with the un-namespaced name as a fallback.
1043+
ClassLoader cl = Thread.currentThread().getContextClassLoader();
1044+
if (cl == null) {
1045+
cl = BeautiDoc.class.getClassLoader();
1046+
}
1047+
for (String candidate : new String[] {"beast.fx/" + resourcePath, resourcePath}) {
1048+
try (InputStream is = cl.getResourceAsStream(candidate)) {
1049+
if (is != null) {
1050+
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
1051+
}
1052+
} catch (IOException e) {
1053+
// try next candidate
1054+
}
1055+
}
9361056
return null;
9371057
}
9381058

0 commit comments

Comments
 (0)