|
22 | 22 | import java.lang.module.ModuleReader; |
23 | 23 | import java.lang.module.ResolvedModule; |
24 | 24 | import java.lang.reflect.InvocationTargetException; |
| 25 | +import java.net.URL; |
25 | 26 | import java.net.URLDecoder; |
26 | 27 | import java.nio.charset.StandardCharsets; |
| 28 | +import java.util.zip.ZipEntry; |
| 29 | +import java.util.zip.ZipFile; |
27 | 30 | import java.util.ArrayList; |
| 31 | +import java.util.Enumeration; |
28 | 32 | import java.util.Arrays; |
29 | 33 | import java.util.Collection; |
30 | 34 | import java.util.HashMap; |
@@ -790,6 +794,97 @@ private static class ModuleTemplateSource extends TemplateSource { |
790 | 794 | @Override String describe() { return "module resource " + resource.path + " in " + resource.moduleName; } |
791 | 795 | } |
792 | 796 |
|
| 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 | + |
793 | 888 | /** Candidate directories that may hold an {@code fxtemplates/} folder: the |
794 | 889 | * classpath-derived directories (covering IDE/dev and jpackage layouts) plus |
795 | 890 | * the installed BEAST package directories. Insertion order is preserved so |
@@ -862,6 +957,14 @@ private static List<TemplateSource> getTemplateSources() { |
862 | 957 | byName.put(base, new ModuleTemplateSource(base, res)); |
863 | 958 | } |
864 | 959 | } |
| 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 | + } |
865 | 968 | return new ArrayList<>(byName.values()); |
866 | 969 | } |
867 | 970 |
|
@@ -933,6 +1036,23 @@ private static String loadResourceFromModules(String resourcePath) { |
933 | 1036 | } |
934 | 1037 | } |
935 | 1038 | } |
| 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 | + } |
936 | 1056 | return null; |
937 | 1057 | } |
938 | 1058 |
|
|
0 commit comments