Skip to content

Commit 8570c76

Browse files
authored
Merge pull request #123 from CompEvol/fix/classpath-template-loading
Load BEAUti templates from the class path, not only module layers
2 parents 862bd9e + edfc4a3 commit 8570c76

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)