diff --git a/config/pom.xml b/config/pom.xml index 9906565..1fdf790 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -35,6 +35,12 @@ jenny-api ${project.version} provided + + + ${project.groupId} + jenny-product + ${project.version} + provided com.sshtools @@ -45,8 +51,8 @@ com.sshtools - jini - 0.2.2 + jini-config + 0.3.3-SNAPSHOT diff --git a/config/src/main/java/com/sshtools/jenny/config/Config.java b/config/src/main/java/com/sshtools/jenny/config/Config.java index e27381b..6d400bd 100644 --- a/config/src/main/java/com/sshtools/jenny/config/Config.java +++ b/config/src/main/java/com/sshtools/jenny/config/Config.java @@ -15,6 +15,9 @@ */ package com.sshtools.jenny.config; +import static com.sshtools.bootlace.api.PluginContext.$; + +import java.io.Closeable; import java.io.IOException; import java.io.StringReader; import java.io.UncheckedIOException; @@ -38,9 +41,11 @@ import com.sshtools.bootlace.api.Plugin; import com.sshtools.bootlace.api.PluginContext; import com.sshtools.jenny.api.ApiLog; +import com.sshtools.jenny.product.Product; import com.sshtools.jini.INI; import com.sshtools.jini.INIReader; import com.sshtools.jini.INIWriter; +import com.sshtools.jini.config.INISet; public class Config implements Plugin { @@ -48,16 +53,22 @@ public class Config implements Plugin { private final static Log LOG = Logs.of(ApiLog.CONFIG); - public interface Handle { + public interface Handle extends Closeable { INI ini(); void store(); + + @Override + void close(); } private record Key(Plugin plugin, Scope scope) { } + + private final Product product = $().plugin(Product.class); + @Deprecated private final Map config = new ConcurrentHashMap<>(); private URLClassLoader bundleLoader; private final ResourceBundle emptyBundle; @@ -88,7 +99,16 @@ public ResourceBundle bundle(Plugin plugin, Locale locale) { return emptyBundle; } } + + public INISet.Builder defaultConfig() { + return new INISet.Builder(product.info().app()); + } + + public INISet.Builder configBuilder(String name) { + return new INISet.Builder(name).withApp(product.info().app()); + } + @Deprecated public Handle config(Plugin plugin, Scope scope) { synchronized (config) { @@ -122,6 +142,11 @@ public INI ini() { public String toString() { return iniFile.toString(); } + + @Override + public void close() { + config.remove(key); + } }; config.put(key, cfg); } diff --git a/config/src/main/java/module-info.java b/config/src/main/java/module-info.java index 3411ffb..aeb5a2f 100644 --- a/config/src/main/java/module-info.java +++ b/config/src/main/java/module-info.java @@ -21,7 +21,10 @@ opens com.sshtools.jenny.config; requires transitive com.sshtools.jenny.api; + requires transitive com.sshtools.jenny.product; requires transitive com.sshtools.jini; + requires transitive com.sshtools.jini.schema; + requires transitive com.sshtools.jini.config; provides Plugin with Config; } diff --git a/config/src/main/resources/layers.ini b/config/src/main/resources/layers.ini index 0d1728d..1a24365 100644 --- a/config/src/main/resources/layers.ini +++ b/config/src/main/resources/layers.ini @@ -7,4 +7,6 @@ description = Configuration framework, providing each plugin their own INI configuration file(s). [artifacts] - ;com.sshtools:jini:0.2.2 + ;com.sshtools:jini-lib:0.3.2 + ;com.sshtools:jini-schema:0.3.2 + ;com.sshtools:jini-config:0.3.2 diff --git a/frameworks/pom.xml b/frameworks/pom.xml index d829e4a..060df95 100644 --- a/frameworks/pom.xml +++ b/frameworks/pom.xml @@ -36,5 +36,6 @@ tty vfs tunnels + product diff --git a/frameworks/product/.gitignore b/frameworks/product/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/frameworks/product/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/frameworks/product/pom.xml b/frameworks/product/pom.xml new file mode 100644 index 0000000..088d127 --- /dev/null +++ b/frameworks/product/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.sshtools + jenny + 0.0.1-SNAPSHOT + ../ + + Jenny - Product + jenny-product + + + + + ${project.groupId} + jenny-api + ${project.version} + provided + + + com.sshtools + bootlace-api + provided + + + + diff --git a/frameworks/product/src/main/java/com/sshtools/jenny/product/Product.java b/frameworks/product/src/main/java/com/sshtools/jenny/product/Product.java new file mode 100644 index 0000000..a202500 --- /dev/null +++ b/frameworks/product/src/main/java/com/sshtools/jenny/product/Product.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2023 JAdaptive Limited (support@jadaptive.com) + * + * Licensed 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 com.sshtools.jenny.product; + +import java.util.Optional; + +import com.sshtools.bootlace.api.Plugin; +import com.sshtools.bootlace.api.PluginContext; + +public final class Product implements Plugin { + + private Info info; + + public record Info(String app, String vendor) { + } + + public final static class Builder { + + private Optional app = Optional.empty(); + private Optional> appClass = Optional.empty(); + private Optional vendor = Optional.empty(); + + public Builder withApp(Class appClass) { + this.appClass = Optional.of(appClass); + return this; + } + + public Builder withApp(String app) { + this.app = Optional.of(app); + return this; + } + + public Builder withVendor(String vendor) { + this.vendor = Optional.of(vendor); + return this; + } + + public Info build() { + return new Info( + app.or(() -> appClass.map(Class::getName)).orElseThrow(() -> new IllegalStateException("'App' must be set, either a class or short ID.")), + vendor.orElse("Unknown") + ); + } + } + + public Product() { + } + + public Product(Info info) { + info(info); + } + + @Override + public void afterOpen(PluginContext context) throws Exception { + } + + public void info(Info info) { + if(this.info != null) + throw new IllegalStateException("Product already registered."); + this.info = info; + } + + public Info info() { + return info; + } + +} diff --git a/frameworks/product/src/main/java/module-info.java b/frameworks/product/src/main/java/module-info.java new file mode 100644 index 0000000..7ac8d60 --- /dev/null +++ b/frameworks/product/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2023 JAdaptive Limited (support@jadaptive.com) + * + * Licensed 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. + */ + +module com.sshtools.jenny.product { + exports com.sshtools.jenny.product; + opens com.sshtools.jenny.product; + + requires transitive com.sshtools.jenny.api; + +} diff --git a/frameworks/product/src/main/resources/layers.ini b/frameworks/product/src/main/resources/layers.ini new file mode 100644 index 0000000..f0240f3 --- /dev/null +++ b/frameworks/product/src/main/resources/layers.ini @@ -0,0 +1,9 @@ +[component] + id = com.sshtools:jenny-product + name = Product + +[meta] + icon = plugin + description = Framework for providing details about the concrete product running on the Jenny framework. + +[artifacts] diff --git a/frameworks/tty/src/main/java/com/sshtools/jenny/tty/Tty.java b/frameworks/tty/src/main/java/com/sshtools/jenny/tty/Tty.java index 6586dbd..58b619c 100644 --- a/frameworks/tty/src/main/java/com/sshtools/jenny/tty/Tty.java +++ b/frameworks/tty/src/main/java/com/sshtools/jenny/tty/Tty.java @@ -15,6 +15,8 @@ */ package com.sshtools.jenny.tty; +import java.text.MessageFormat; + import com.sshtools.bootlace.api.Logs; import com.sshtools.bootlace.api.Logs.Log; import com.sshtools.bootlace.api.Plugin; @@ -38,8 +40,8 @@ public void provider(TtyProvider provider) { this.provider = provider; } else if (this.provider != null && provider == null) { this.provider = null; - } else { - throw new IllegalStateException(); + } else if(this.provider != null) { + throw new IllegalStateException(MessageFormat.format("Cannot be more than one Tty provider. {0} and {1}", this.provider.getClass().getName(), provider.getClass().getName())); } } diff --git a/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/Bootstrap5.java b/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/Bootstrap5.java index b53062b..fd9b15a 100644 --- a/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/Bootstrap5.java +++ b/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/Bootstrap5.java @@ -21,6 +21,8 @@ import com.sshtools.bootlace.api.Plugin; import com.sshtools.jenny.web.NpmWebModule; import com.sshtools.jenny.web.WebModule; +import com.sshtools.jenny.web.WebModule.Type; +import com.sshtools.tinytemplate.bootstrap.forms.Form; public class Bootstrap5 implements Plugin { @@ -33,6 +35,7 @@ public class Bootstrap5 implements Plugin { public final static WebModule MODULE_BOOTSTRAP5 = new NpmWebModule.Builder(). withGAV(ofSpec("npm:bootstrap")). withClass(Bootstrap5.class). + withType(Type.JS). withMain("dist/js/bootstrap.bundle.min.js"). withRequires(MODULE_JQUERY). build(); @@ -46,7 +49,7 @@ public class Bootstrap5 implements Plugin { public final static WebModule MODULE_BOOTSTRAP_TABLE = new NpmWebModule.Builder(). withMain("dist/bootstrap-table.min.js"). - withType(""). + withType(Type.JS). withGAV(ofSpec("npm:bootstrap-table")). withClass(Bootstrap5.class). withRequires(MODULE_BOOTSTRAP5). @@ -66,5 +69,10 @@ public class Bootstrap5 implements Plugin { MODULE_BOOTSTRAP5 ); - + public final static WebModule MODULE_TTBS = WebModule.of( + "/ttbs.js", + Form.class, + "ttbs.js", + Bootstrap5.MODULE_BOOTSTRAP5 + ); } diff --git a/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/TemplatedFormDataReceiver.java b/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/TemplatedFormDataReceiver.java new file mode 100644 index 0000000..c27ed72 --- /dev/null +++ b/plugins/bootstrap5/src/main/java/com/sshtools/jenny/bootstrap5/TemplatedFormDataReceiver.java @@ -0,0 +1,69 @@ +package com.sshtools.jenny.bootstrap5; + +import java.util.function.Consumer; + +import com.sshtools.tinytemplate.Templates.TemplateModel; +import com.sshtools.tinytemplate.bootstrap.forms.Form.FormDataReceiver; +import com.sshtools.tinytemplate.bootstrap.forms.Form.FormFile; +import com.sshtools.tinytemplate.bootstrap.forms.Form.Results; +import com.sshtools.tinytemplate.bootstrap.forms.InputType; +import com.sshtools.tinytemplate.bootstrap.forms.Text; +import com.sshtools.tinytemplate.bootstrap.forms.Validation.ValidationException; +import com.sshtools.uhttpd.UHTTPD.FormData; +import com.sshtools.uhttpd.UHTTPD.Transaction; + +public class TemplatedFormDataReceiver { + + public static void alertForResults(Class base, TemplateModel template, Results res, Consumer> onOk, String errSuffix) { + if(res.ok()) { + template.variable("validated", true); + Alerts.alertable(base, template, () -> { + try { + onOk.accept(res); + } + catch(ValidationException ve) { + template.include("alerts", () -> Alerts.danger(base, "error." + errSuffix, + ve.text().map(Text::resolveString).orElse(""))); + } + }); + } + else { + var errs = res.results(); + if(errs.size() == 1) { + template.include("alerts", () -> Alerts.danger(base, "error." + errSuffix, + errs.get(0).firstError().text().map(Text::resolveString).orElse(""))); + } + else { + template.include("alerts", () -> Alerts.danger(base, "errors." + errSuffix, errs.size())); + } + } + } + + + @SuppressWarnings("resource") + public static Consumer txFormDataReceiver(Transaction tx) { + return fdr -> { + for(var part : tx.request().asBufferedParts()) { + if(part instanceof FormData fd) { + var field = fdr.field(part.name()); + if(field != null) { + if(field.resolveInputType() == InputType.FILE) + fdr.file(field, + new FormFile( + fd.filename().orElse("upload"), + fd.contentType().orElse("application/octet-stream"), + -1, + fd.asStream() + ) + ); + else + fdr.field(field, part.asString()); + } + } + else + throw new UnsupportedOperationException("todo"); + } + }; + } + +} diff --git a/plugins/bootstrap5/src/main/java/module-info.java b/plugins/bootstrap5/src/main/java/module-info.java index 4663383..0326f16 100644 --- a/plugins/bootstrap5/src/main/java/module-info.java +++ b/plugins/bootstrap5/src/main/java/module-info.java @@ -21,6 +21,7 @@ opens com.sshtools.jenny.bootstrap5; requires transitive com.sshtools.jenny.web; + requires transitive com.sshtools.tinytemplate.bootstrap.forms; provides Plugin with Bootstrap5; } diff --git a/plugins/i18n/src/main/java/com/sshtools/jenny/i18n/I18N.java b/plugins/i18n/src/main/java/com/sshtools/jenny/i18n/I18N.java index 36e8042..bb189d0 100644 --- a/plugins/i18n/src/main/java/com/sshtools/jenny/i18n/I18N.java +++ b/plugins/i18n/src/main/java/com/sshtools/jenny/i18n/I18N.java @@ -15,6 +15,8 @@ */ package com.sshtools.jenny.i18n; +import java.util.UUID; + import javax.json.Json; import com.sshtools.bootlace.api.Logs; @@ -33,6 +35,8 @@ public class I18N implements Plugin { private Web web; private WebModule webModule; + + final static String RNDTKN = UUID.randomUUID().toString().replace("-", ""); @Override public void afterOpen(PluginContext context) { @@ -55,7 +59,7 @@ public Handler i18NScript(Class bundle) { buf.append("if(typeof i18n === 'undefined') { alert('I18N Javascript support not loaded.'); } else { i18n.bundles['"); buf.append(bundle.getSimpleName()); buf.append("'] = JSON.parse('"); - buf.append(arrBldr.build().toString().replace("'", "\\'")); + buf.append(arrBldr.build().toString().replace("\\\"", RNDTKN).replace("\"", "\\\"")); buf.append("'); }"); tx.response("text/javascript", buf); diff --git a/plugins/io/src/main/java/com/sshtools/jenny/io/Io.java b/plugins/io/src/main/java/com/sshtools/jenny/io/Io.java index 715009a..577d86f 100644 --- a/plugins/io/src/main/java/com/sshtools/jenny/io/Io.java +++ b/plugins/io/src/main/java/com/sshtools/jenny/io/Io.java @@ -44,6 +44,8 @@ public class Io implements Plugin { final static Log LOG = Logs.of(Category.ofName(Io.class)); + + public final static WebModule MODULE_IO = WebModule.of("/io/io.js", Io.class, "io.js"); private record SocketChannelKey(WebSocket socket, String channel) { } @@ -118,7 +120,6 @@ public static IoChannel of(Sender sender, Consumer receiver, Consumer private final WebSocketHandler io; private Web web; private final Map channels = new ConcurrentHashMap<>(); - private WebModule webModule; public Io() { webSockets = new CopyOnWriteArrayList<>(); @@ -163,16 +164,15 @@ public WebSocketHandler io() { @Override public void open(PluginContext context) { web = context.plugin(Web.class); - webModule = WebModule.of("/io/io.js", Io.class, "io.js"); context.autoClose( - web.modules(webModule), + web.modules(MODULE_IO), web.router().route(). webSocket("/io/io", io).build() ); } public WebModule webModule() { - return webModule; + return MODULE_IO; } /** diff --git a/plugins/jobs/src/main/java/com/sshtools/jenny/jobs/Jobs.java b/plugins/jobs/src/main/java/com/sshtools/jenny/jobs/Jobs.java index 27a9c51..7fd1d03 100644 --- a/plugins/jobs/src/main/java/com/sshtools/jenny/jobs/Jobs.java +++ b/plugins/jobs/src/main/java/com/sshtools/jenny/jobs/Jobs.java @@ -61,13 +61,14 @@ public class Jobs implements Plugin { final static Log LOG = Logs.of(WebLog.JOBS); - public record JobOptions(Queue queue, Optional bundle, Job job, Optional category) {} + public record JobOptions(Queue queue, Optional bundle, Job job, Optional category, boolean exclusive) {} public final static class JobBuilder { private Queue queue = StandardQueues.GENERIC; private Optional bundle = Optional.empty(); private final Job job; private Optional category = Optional.empty(); + private boolean exclusive = true; public JobBuilder(Job job) { this.job = job; @@ -88,6 +89,15 @@ public JobBuilder withCategory(String category) { return this; } + public JobBuilder withoutExclusive() { + return withExclusive(false); + } + + public JobBuilder withExclusive(boolean exclusive) { + this.exclusive = exclusive; + return this; + } + public JobBuilder withQueue(Queue queue) { this.queue = queue; return this; @@ -103,7 +113,7 @@ public JobBuilder withBundle(Class bundle, Locale locale) { } public JobOptions build() { - return new JobOptions<>(queue, bundle, job, category); + return new JobOptions<>(queue, bundle, job, category, exclusive); } } @@ -170,7 +180,7 @@ public Handle run(JobOptions options) { var q = options.queue; var job = options.job; - if(jobs.containsKey(jobCategory)) + if(jobs.containsKey(jobCategory) && options.exclusive) throw new IllegalStateException(MessageFormat.format("Job with ID {0}", jobCategory)); ScheduledExecutorService queue; synchronized(queues) { @@ -250,7 +260,7 @@ public void result(Object result) { private void sendUpdate() { var sndr = ioSenders.get(jobCategory); if(sndr == null) - LOG.debug("Attempt to send update before sender was ready for {0}", jobCategory); + LOG.warning("Attempt to send update before sender was ready for {0}", jobCategory); else { sndr.send(Json.createObjectBuilder(). add("type", "update"). @@ -275,6 +285,10 @@ public void onCancel(Function, Boolean> r) { job.apply(ctx); return state.result; } + catch(Throwable e) { + LOG.error("Job failure.", e); + throw e; + } finally { var hndl = jobsByUuid.remove(state.uuid()); synchronized(jobs) { diff --git a/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.html b/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.html index a0892bb..3dfeaaf 100644 --- a/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.html +++ b/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.html @@ -3,14 +3,14 @@
-
- ${job.title} +
+ ${job.title}
-
-
${job.text}
+
@@ -19,6 +19,11 @@ ${%cancel}
+
+
+ ${job.text} +
+
\ No newline at end of file diff --git a/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.js b/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.js index f23f364..8745221 100644 --- a/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.js +++ b/plugins/jobs/src/main/resources/com/sshtools/jenny/jobs/job-progress.frag.js @@ -14,8 +14,10 @@ io.onReady(function(io) { bar.find('.job-title').html(msg.title); var br = $(bar.find('.progress-bar')); - br.html(msg.text); br.css('width', msg.percent + '%'); + + var tx = $(bar.find('.progress-text')); + tx.html(msg.text); } else if (msg.type === 'complete') { window.location.reload(); diff --git a/pom.xml b/pom.xml index 81d7f24..60e51f6 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,6 @@ auth plugins package - config UTF-8 diff --git a/web/pom.xml b/web/pom.xml index a1707ab..801b32d 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -60,6 +60,11 @@ jenny-config ${project.version} + + ${project.groupId} + jenny-product + ${project.version} + diff --git a/web/src/main/java/com/sshtools/jenny/web/NpmWebModule.java b/web/src/main/java/com/sshtools/jenny/web/NpmWebModule.java index 693cb03..3bdcfe7 100644 --- a/web/src/main/java/com/sshtools/jenny/web/NpmWebModule.java +++ b/web/src/main/java/com/sshtools/jenny/web/NpmWebModule.java @@ -17,6 +17,8 @@ import java.util.Properties; import java.util.Set; +import javax.json.Json; + import com.sshtools.bootlace.api.GAV; import com.sshtools.jenny.web.WebModule.Type; import com.sshtools.jenny.web.WebModule.WebModuleResource; @@ -27,18 +29,25 @@ public enum Compression { AUTO, NONE, MINIFY } + public static String toNpm(GAV gav) { + if(gav.groupId().equals("npm")) { + return gav.artifactId(); + } + else { + return "@" + gav.groupId().substring(4) + "/" + gav.artifactId(); + } + } public final static class Builder { private Optional main = Optional.empty(); private Optional module = Optional.empty(); private Optional style = Optional.empty(); - private Optional type = Optional.empty(); + private Optional type = Optional.empty(); private Optional sass = Optional.empty(); private Optional loader = Optional.empty(); private Optional gav = Optional.empty(); private List resources = new ArrayList(); private final Set requires = new LinkedHashSet<>(); - private boolean preferJsModule = false; private Compression compression = Compression.AUTO; public Builder withCompression(Compression compression) { @@ -55,19 +64,6 @@ public Builder withRequires(Collection requires) { return this; } - public Builder withPreferJs() { - return withPreferJsModule(false); - } - - public Builder withPreferJsModule() { - return withPreferJsModule(true); - } - - public Builder withPreferJsModule(boolean preferJsModule) { - this.preferJsModule = preferJsModule; - return this; - } - public Builder withGAV(String... parts) { return withGAV(GAV.ofParts(parts)); } @@ -77,6 +73,20 @@ public Builder withGAV(GAV gav) { return this; } + public Builder withPackage(String name) { + if(name.startsWith("@")) { + var idx = name.indexOf('/'); + return withPackage(name.substring(1, idx), name.substring(idx + 1)); + } + else { + return withGAV(GAV.ofParts("npm", name)); + } + } + + public Builder withPackage(String scope, String name) { + return withGAV(GAV.ofParts("npm." + scope, name)); + } + public Builder withClass(Class clazz) { return withLoader(clazz.getClassLoader()); } @@ -101,7 +111,7 @@ public Builder withStyle(String style) { return this; } - public Builder withType(String type) { + public Builder withType(Type type) { this.type = Optional.of(type); return this; } @@ -157,28 +167,71 @@ public WebModule build() { var main = this.main.orElseGet(() -> locateBest(props, items, "main", "js")); var style = this.style.orElseGet(() -> locateBest(props, items, "style", "css")); - var type = this.type.orElseGet(() -> props.getProperty("type")); var module = this.module.orElseGet(() -> locateBest(props, items, "module", "js")); + var type = this.type.orElseGet(() -> { + try { + return Type.valueOf(props.getProperty("type").toUpperCase()); + } + catch(Exception e) { + return module != null ? Type.MODULE : Type.JS; + } + }); var resource = props.getProperty("resource"); + var useModule = type == Type.MODULE || type == Type.IMPORTED; var bldr = new WebModule.Builder(); - bldr.withName(gav.toString()); + bldr.withName(toNpm(gav)); bldr.withLoader(loader); bldr.addResources(this.resources); bldr.withUri("/" + resource); bldr.withRequires(this.requires); bldr.asDirectory(); - if(module != null && (main == null || preferJsModule ) ) { + var packages = props.getProperty("resource") + "/package.json"; + var pkgres = loader.getResource(packages); + if(pkgres != null && useModule) { + try(var json = Json.createReader(pkgres.openStream())) { + var mfest = json.readObject(); + var deps = mfest.getJsonObject("dependencies"); + if(deps != null) { + for(var dep : deps.keySet()) { + String group; + String art; + var idx = dep.indexOf('/'); + if(idx == -1) { + group = "npm"; + art = dep; + } + else { + group = "npm." + dep.substring(1, idx); + art = dep.substring(idx + 1); + } + + bldr.withRequires(new NpmWebModule.Builder(). + withLoader(loader). + withGAV(GAV.ofParts(group, art)). + withType(type). + build()); + } + } + } + } + + if(type == Type.IMPORTED) { bldr.addResources(new WebModuleResource.Builder(). - withType(Type.JS_MODULE). + withType(type). withResource(normalize(module)). build()); } - - if(main != null && ( module == null || !preferJsModule ) ) { + else if(module != null && type == Type.MODULE ) { + bldr.addResources(new WebModuleResource.Builder(). + withType(type). + withResource(normalize(module)). + build()); + } + else if(main != null && ( type == Type.JS || type == Type.MODULE ) ) { bldr.addResources(new WebModuleResource.Builder(). - withType("module".equals(type) ? Type.JS_MODULE : Type.JS). + withType(type). withResource(normalize(main)). build()); } diff --git a/web/src/main/java/com/sshtools/jenny/web/Web.java b/web/src/main/java/com/sshtools/jenny/web/Web.java index 905e6f7..5e06cb1 100644 --- a/web/src/main/java/com/sshtools/jenny/web/Web.java +++ b/web/src/main/java/com/sshtools/jenny/web/Web.java @@ -37,6 +37,9 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; +import javax.json.Json; +import javax.json.JsonObjectBuilder; + import com.sshtools.bootlace.api.ConfigResolver.Scope; import com.sshtools.bootlace.api.DependencyGraph; import com.sshtools.bootlace.api.Logs; @@ -294,14 +297,14 @@ private void configureServer(RootContextBuilder bldr) { var http = webConfig.ini().sectionOr("http"); http.ifPresent(cfg -> { - bldr.withHttp(cfg.getIntOr("port", 8080)); - bldr.withHttpAddress(cfg.getOr("address", "::")); + bldr.withHttp(cfg.getInt("port", 8080)); + bldr.withHttpAddress(cfg.get("address", "::")); }); var https = webConfig.ini().sectionOr("https"); https.ifPresent(cfg -> { - bldr.withHttps(cfg.getIntOr("port", 8443)); - bldr.withHttpsAddress(cfg.getOr("address", "::")); + bldr.withHttps(cfg.getInt("port", 8443)); + bldr.withHttpsAddress(cfg.get("address", "::")); cfg.getOr("key-password").ifPresent(kp -> bldr.withKeyPassword(kp.toCharArray())); cfg.getOr("keystore-file").ifPresent(ks -> bldr.withKeyStoreFile(Paths.get(ks))); cfg.getOr("keystore-password").ifPresent(kp -> bldr.withKeyPassword(kp.toCharArray())); @@ -309,19 +312,19 @@ private void configureServer(RootContextBuilder bldr) { }); webConfig.ini().sectionOr("tuning").ifPresent(cfg -> { - if(!cfg.getBooleanOr("compression", true)) { + if(!cfg.getBoolean("compression", true)) { bldr.withoutCompression(); } }); webConfig.ini().sectionOr("ncsa").ifPresent(cfg -> { bldr.withLogger(new NCSALoggerBuilder(). - withAppend(cfg.getBooleanOr("append", true)). - withDirectory(Paths.get(cfg.getOr("directory", System.getProperty("user.dir") + File.separator + "logs"))). - withExtended(cfg.getBooleanOr("extended", true)). - withServerName(cfg.getBooleanOr("server-name", false)). - withFilenamePattern(cfg.getOr("pattern", "access_log_%d.log")). - withFilenameDateFormat(cfg.getOr("date-format", "ddMM")). + withAppend(cfg.getBoolean("append", true)). + withDirectory(Paths.get(cfg.get("directory", System.getProperty("user.dir") + File.separator + "logs"))). + withExtended(cfg.getBoolean("extended", true)). + withServerName(cfg.getBoolean("server-name", false)). + withFilenamePattern(cfg.get("pattern", "access_log_%d.log")). + withFilenameDateFormat(cfg.get("date-format", "ddMM")). build()); }); } @@ -400,12 +403,22 @@ private TemplateModel fragHead(Transaction tx) { private List scriptModules(Transaction tx, String content, Placement placement) { - var l = sortModules(Arrays.asList(Type.JS, Type.JS_MODULE), placement); + var l = sortModules(Arrays.asList(Type.IMPORT_MAP, Type.IMPORTED, Type.JS, Type.MODULE), placement); return l.stream(). - map(mod -> TemplateModel.ofContent(content). - variable("type", mod.scriptType()). - variable("src", mod.uri()) + map(mod -> { + var mdl = TemplateModel.ofContent(content). + variable("type", mod.scriptType()); + + if(mod.contentOr().isPresent()) { + mdl.variable("content", mod.content()); + } + else { + mdl.variable("src", mod.uri()); + } + + return mdl; + } ).toList(); } @@ -435,12 +448,41 @@ private Collection sortModules(Collection types, Placem /* Topological DAG sort */ l = new DependencyGraph<>(l).getTopologicallySorted(); + /* Build IMPORT_MAP if there are IMPORTED modules */ + l = buildImportMap(l); + return new LinkedHashSet<>(l. stream(). flatMap(a -> a.resources().stream()). - filter(m -> m.placement().equals(placement) && types.contains(m.type())). + filter(m -> m.type() != Type.IMPORTED && m.placement().equals(placement) && types.contains(m.type())). toList(). reversed() ); } + + private List buildImportMap(List wms) { + var l = new ArrayList(wms); + JsonObjectBuilder imports = null; + for(var wm : wms) { + for(var res : wm.resources()) { + if(res.type() == Type.IMPORTED) { + if(imports == null) { + imports = Json.createObjectBuilder(); + } + imports.add(wm.name(), res.uri()); + } + } + } + if(imports != null) { + l.add(new WebModule.Builder(). + withResources(new WebModuleResource.Builder(). + withType(Type.IMPORT_MAP). + withContent(Json.createObjectBuilder(). + add("imports", imports.build()). + build().toString()). + build()). + build()); + } + return l; + } } diff --git a/web/src/main/java/com/sshtools/jenny/web/WebModule.java b/web/src/main/java/com/sshtools/jenny/web/WebModule.java index 31cba13..2a11050 100644 --- a/web/src/main/java/com/sshtools/jenny/web/WebModule.java +++ b/web/src/main/java/com/sshtools/jenny/web/WebModule.java @@ -42,11 +42,11 @@ public final class WebModule implements NodeModel { public enum Mount { - FILE, DIRECTORY + FILE, DIRECTORY, CONTENT } public enum Type { - CSS, JS, JS_MODULE + IMPORT_MAP, CSS, JS, MODULE, IMPORTED } public enum Placement { @@ -66,7 +66,7 @@ public static WebModule js(String uri, Handler handler, WebModule... requires) { } public static WebModule jsModule(String uri, Handler handler, WebModule... requires) { - return of(uri, handler, Type.JS_MODULE, requires); + return of(uri, handler, Type.MODULE, requires); } public static WebModule of(String uri, Handler handler, Type type, WebModule... requires) { @@ -78,7 +78,7 @@ public static WebModule of(String uri, Handler handler, Type type, WebModule... } public static WebModule jsModule(String uri, Class base, String path, WebModule... requires) { - return of(uri, base, path, Type.JS_MODULE, requires); + return of(uri, base, path, Type.MODULE, requires); } public static WebModule js(String uri, Class base, String path, WebModule... requires) { @@ -112,6 +112,7 @@ public final static class Builder { private Optional placement = Optional.empty(); private ResourceRef ref; private Optional handler = Optional.empty(); + private Optional content = Optional.empty(); public Builder withHandler(Handler handler) { this.handler = Optional.of(handler); @@ -128,6 +129,11 @@ public Builder withPlacement(Placement placement) { return this; } + public Builder withContent(String content) { + this.content = Optional.of(content); + return this; + } + public Builder withResource(Class resourceParent, String resource) { return withResource(new ResourceRef(resourceParent, resource)); } @@ -163,7 +169,7 @@ public static WebModuleResource js(String resource) { } public static WebModuleResource jsModule(String resource) { - return new Builder().withResource(resource).withType(Type.JS_MODULE).build(); + return new Builder().withResource(resource).withType(Type.MODULE).build(); } public static WebModuleResource of(Class resourceParent, String resource) { @@ -183,7 +189,7 @@ public static WebModuleResource js(Class resourceParent, String resource) { } public static WebModuleResource jsModule(Class resourceParent, String resource) { - return new Builder().withResource(resourceParent, resource).withType(Type.JS_MODULE).build(); + return new Builder().withResource(resourceParent, resource).withType(Type.MODULE).build(); } public static WebModuleResource of(ResourceRef ref) { @@ -211,7 +217,7 @@ public static WebModuleResource css(String path, Handler handler) { } public static WebModuleResource jsModule(String path, Handler handler) { - return of(path, handler, Type.JS_MODULE); + return of(path, handler, Type.MODULE); } public static WebModuleResource of(String path, Handler handler, Type type) { @@ -226,16 +232,26 @@ public static WebModuleResource of(String path, Handler handler, Type type) { private final Optional type; private final ResourceRef ref; private final Optional handler; + private final Optional content; private WebModule module; private WebModuleResource(Builder builder) { this.ref = builder.ref; - if(ref == null && builder.handler.isEmpty()) { - throw new IllegalStateException("Must either have a resource reference or a handler."); + if(ref == null && builder.handler.isEmpty() && builder.content.isEmpty()) { + throw new IllegalStateException("Must either have a resource reference, content or a handler."); } this.placement = builder.placement; this.type = builder.type; this.handler = builder.handler; + this.content = builder.content; + } + + public String content() { + return content.get(); + } + + public Optional contentOr() { + return content; } public Placement placement() { @@ -265,9 +281,12 @@ public String scriptType() { if(type.equals(Type.JS)) { return "text/javascript"; } - else if(type.equals(Type.JS_MODULE)) { + else if(type.equals(Type.MODULE)) { return "module"; } + else if(type.equals(Type.IMPORT_MAP)) { + return "importmap"; + } else throw new IllegalStateException("Not a script type."); } @@ -368,14 +387,14 @@ private WebModule(Builder builder) { this.resources = Collections.unmodifiableList(new ArrayList<>(builder.resources)); this.resources.forEach(r -> r.module = this); - this.mount = builder.mount.orElseGet(() -> builder.uri.endsWith("/") ? Mount.DIRECTORY : Mount.FILE); + this.mount = builder.mount.orElseGet(() -> builder.uri == null ? Mount.CONTENT : builder.uri.endsWith("/") ? Mount.DIRECTORY : Mount.FILE); if(mount == Mount.FILE && resources.size() != 1) { throw new IllegalStateException(MessageFormat.format("Mount ''{0}'' must specify exactly on resource to map to, there are {1}", Mount.FILE, resources.size())); } var uri = builder.uri; - if(!uri.startsWith("/")) { + if(uri != null && !uri.startsWith("/")) { uri = "/" + uri; } @@ -389,7 +408,7 @@ private WebModule(Builder builder) { // var firstRes = resources.get(0); } - var pattern = escapeLiteral(Objects.requireNonNull(uri)); + var pattern = uri == null ? null : escapeLiteral(Objects.requireNonNull(uri)); if(mount == Mount.DIRECTORY) { pattern += "(.*)"; } @@ -444,6 +463,9 @@ public void get(Transaction req) throws Exception { }; } } + else if(mount == Mount.CONTENT) { + this.handler = (c) -> {}; + } else { /** * Web module is a single mapping from a uri to a WebModuleResource diff --git a/web/src/main/resources/com/sshtools/jenny/web/bodyhead.frag.html b/web/src/main/resources/com/sshtools/jenny/web/bodyhead.frag.html index e236b8d..4fdeadd 100644 --- a/web/src/main/resources/com/sshtools/jenny/web/bodyhead.frag.html +++ b/web/src/main/resources/com/sshtools/jenny/web/bodyhead.frag.html @@ -1,3 +1,3 @@ - + diff --git a/web/src/main/resources/com/sshtools/jenny/web/bodytail.frag.html b/web/src/main/resources/com/sshtools/jenny/web/bodytail.frag.html index e236b8d..4fdeadd 100644 --- a/web/src/main/resources/com/sshtools/jenny/web/bodytail.frag.html +++ b/web/src/main/resources/com/sshtools/jenny/web/bodytail.frag.html @@ -1,3 +1,3 @@ - + diff --git a/web/src/main/resources/com/sshtools/jenny/web/head.frag.html b/web/src/main/resources/com/sshtools/jenny/web/head.frag.html index c801e86..5257aa9 100644 --- a/web/src/main/resources/com/sshtools/jenny/web/head.frag.html +++ b/web/src/main/resources/com/sshtools/jenny/web/head.frag.html @@ -1,7 +1,13 @@ - + + + + + + +