diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/Config.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/Config.java new file mode 100644 index 0000000..fef668a --- /dev/null +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/Config.java @@ -0,0 +1,107 @@ +package com.annimon.ownlang.modules.server; + +import com.annimon.ownlang.lib.ArrayValue; +import com.annimon.ownlang.lib.NumberValue; +import com.annimon.ownlang.lib.Types; +import com.annimon.ownlang.lib.Value; +import io.javalin.config.JavalinConfig; +import io.javalin.http.staticfiles.Location; +import java.util.Map; +import java.util.function.Consumer; + +/* + * Sample config: + * { + * "webjars": true, + * "classpathDirs": ["dir1", "dir2"], + * "externalDirs": ["dir1", "dir2"], + * + * "asyncTimeout": 6_000, + * "defaultContentType": "text/plain", + * "etags": true, + * "maxRequestSize": 1_000_000, + * + * "caseInsensitiveRoutes": true, + * "ignoreTrailingSlashes": true, + * "multipleSlashesAsSingle": true, + * "contextPath": "/", + * + * "basicAuth": ["user", "password"], + * "dev": true, + * "showBanner": false, + * "sslRedirects": true + * } + */ + +class Config { + private final Map map; + + public Config(Map map) { + this.map = map; + } + + public void setup(JavalinConfig config) { + // staticFiles + ifTrue("webjars", config.staticFiles::enableWebjars); + ifArray("classpathDirs", directories -> { + for (Value directory : directories) { + config.staticFiles.add(directory.asString(), Location.CLASSPATH); + } + }); + ifArray("externalDirs", directories -> { + for (Value directory : directories) { + config.staticFiles.add(directory.asString(), Location.EXTERNAL); + } + }); + + // http + ifNumber("asyncTimeout", value -> config.http.asyncTimeout = value.asLong()); + ifString("defaultContentType", value -> config.http.defaultContentType = value); + ifBoolean("etags", flag -> config.http.generateEtags = flag); + ifNumber("maxRequestSize", value -> config.http.maxRequestSize = value.asLong()); + + // routing + ifBoolean("caseInsensitiveRoutes", flag -> config.routing.caseInsensitiveRoutes = flag); + ifBoolean("ignoreTrailingSlashes", flag -> config.routing.ignoreTrailingSlashes = flag); + ifBoolean("multipleSlashesAsSingle", flag -> config.routing.treatMultipleSlashesAsSingleSlash = flag); + ifString("contextPath", path -> config.routing.contextPath = path); + + // other + ifArray("basicAuth", arr -> config.plugins.enableBasicAuth(arr.get(0).asString(), arr.get(1).asString())); + ifBoolean("showBanner", flag -> config.showJavalinBanner = flag); + ifTrue("dev", config.plugins::enableDevLogging); + ifTrue("sslRedirects", config.plugins::enableSslRedirects); + } + + private void ifTrue(String key, Runnable action) { + if (map.containsKey(key) && map.get(key).asInt() != 0) { + action.run(); + } + } + + private void ifBoolean(String key, Consumer consumer) { + if (!map.containsKey(key)) return; + consumer.accept(map.get(key).asInt() != 0); + } + + private void ifNumber(String key, Consumer consumer) { + if (!map.containsKey(key)) return; + final Value value = map.get(key); + if (value.type() == Types.NUMBER) { + consumer.accept((NumberValue) value); + } + } + + private void ifString(String key, Consumer consumer) { + if (!map.containsKey(key)) return; + consumer.accept(map.get(key).asString()); + } + + private void ifArray(String key, Consumer consumer) { + if (!map.containsKey(key)) return; + final Value value = map.get(key); + if (value.type() == Types.ARRAY) { + consumer.accept((ArrayValue) value); + } + } +} diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java index 6d8477c..fe9d2ca 100644 --- a/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ContextValue.java @@ -4,45 +4,108 @@ import com.annimon.ownlang.lib.*; import io.javalin.http.Context; import io.javalin.http.HttpStatus; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; -public class ContextValue extends MapValue { +class ContextValue extends MapValue { private final Context ctx; public ContextValue(@NotNull Context ctx) { - super(10); + super(32); this.ctx = ctx; init(); } private void init() { + set("attribute", this::attribute); + set("basicAuthCredentials", this::basicAuthCredentials); set("body", Converters.voidToString(ctx::body)); + set("bodyAsBytes", this::bodyAsBytes); set("characterEncoding", Converters.voidToString(ctx::characterEncoding)); - set("contentType", Converters.voidToString(ctx::contentType)); - set("contextPath", Converters.voidToString(ctx::contextPath)); - set("fullUrl", Converters.voidToString(ctx::fullUrl)); - set("host", Converters.voidToString(ctx::host)); - set("ip", Converters.voidToString(ctx::ip)); - set("matchedPath", Converters.voidToString(ctx::matchedPath)); - set("path", Converters.voidToString(ctx::path)); - set("protocol", Converters.voidToString(ctx::protocol)); - set("queryString", Converters.voidToString(ctx::queryString)); - set("url", Converters.voidToString(ctx::url)); - set("userAgent", Converters.voidToString(ctx::userAgent)); - + set("cookie", this::cookie); set("contentLength", Converters.voidToInt(ctx::contentLength)); - set("port", Converters.voidToInt(ctx::port)); - set("statusCode", Converters.voidToInt(ctx::statusCode)); - + set("contentType", this::contentType); + set("contextPath", Converters.voidToString(ctx::contextPath)); + set("endpointHandlerPath", Converters.voidToString(ctx::endpointHandlerPath)); + set("formParam", Converters.stringToString(ctx::formParam)); + set("fullUrl", Converters.voidToString(ctx::fullUrl)); + set("handlerType", Converters.voidToString(() -> ctx.handlerType().name())); + set("header", this::header); + set("host", Converters.voidToString(ctx::host)); + set("html", stringToContext(ctx::html)); + set("ip", Converters.voidToString(ctx::ip)); set("json", objectToContext(ctx::json)); set("jsonStream", objectToContext(ctx::jsonStream)); - + set("matchedPath", Converters.voidToString(ctx::matchedPath)); + set("path", Converters.voidToString(ctx::path)); + set("port", Converters.voidToInt(ctx::port)); + set("protocol", Converters.voidToString(ctx::protocol)); + set("queryString", Converters.voidToString(ctx::queryString)); + set("redirect", this::redirect); + set("removeCookie", this::removeCookie); set("render", this::render); set("result", this::result); - set("redirect", this::redirect); + set("statusCode", Converters.voidToInt(ctx::statusCode)); + set("scheme", Converters.voidToString(ctx::scheme)); + set("url", Converters.voidToString(ctx::url)); + set("userAgent", Converters.voidToString(ctx::userAgent)); + } + + private Value attribute(Value[] args) { + Arguments.checkOrOr(1, 2, args.length); + String key = args[0].asString(); + if (args.length == 1) { + return ctx.attribute(key); + } else { + ctx.attribute(key, args[1]); + } + return this; + } + + private Value basicAuthCredentials(Value[] args) { + Arguments.check(0, args.length); + final var cred = ctx.basicAuthCredentials(); + return ArrayValue.of(new String[] { + cred.getUsername(), + cred.getPassword() + }); + } + + private Value bodyAsBytes(Value[] args) { + Arguments.check(0, args.length); + return ArrayValue.of(ctx.bodyAsBytes()); + } + + private Value cookie(Value[] args) { + Arguments.checkRange(1, 3, args.length); + if (args.length == 1) { + return new StringValue(ctx.cookie(args[0].asString())); + } + int maxAge = args.length == 3 ? args[2].asInt() : -1; + ctx.cookie(args[0].asString(), args[1].asString(), maxAge); + return this; + } + + private Value contentType(Value[] args) { + Arguments.checkOrOr(0, 1, args.length); + if (args.length == 0) { + return new StringValue(ctx.contentType()); + } else { + ctx.contentType(args[0].asString()); + return this; + } + } + + private Value header(Value[] args) { + Arguments.checkOrOr(1, 2, args.length); + String name = args[0].asString(); + if (args.length == 1) { + return new StringValue(ctx.header(name)); + } else { + ctx.header(name, args[1].asString()); + return this; + } } private Value render(Value[] args) { @@ -51,9 +114,8 @@ public class ContextValue extends MapValue { if (args.length == 1) { ctx.render(filePath); } else { - MapValue map = (MapValue) args[1]; - Map data = new HashMap<>(map.size()); - map.getMap().forEach((k, v) -> data.put(k.asString(), v.asJavaObject())); + MapValue map = ValueUtils.consumeMap(args[1], 1); + Map data = map.convertMap(Value::asString, Value::asJavaObject); ctx.render(filePath, data); } return this; @@ -66,6 +128,14 @@ public class ContextValue extends MapValue { return this; } + private Value removeCookie(Value[] args) { + Arguments.checkOrOr(1, 2, args.length); + String name = args[0].asString(); + String path = args.length == 2 ? args[1].asString() : "/"; + ctx.removeCookie(name, path); + return this; + } + private Value result(Value[] args) { Arguments.checkOrOr(0, 1, args.length); if (args.length == 0) { @@ -81,6 +151,14 @@ public class ContextValue extends MapValue { } } + private Value stringToContext(Consumer consumer) { + return new FunctionValue(args -> { + Arguments.check(1, args.length); + consumer.accept(args[0].asString()); + return this; + }); + } + private Value objectToContext(Consumer consumer) { return new FunctionValue(args -> { Arguments.check(1, args.length); diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java index b9b7a47..05dbcd7 100644 --- a/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/ServerValue.java @@ -1,17 +1,18 @@ package com.annimon.ownlang.modules.server; +import com.annimon.ownlang.exceptions.OwnLangRuntimeException; import com.annimon.ownlang.lib.*; import io.javalin.Javalin; import io.javalin.http.Handler; import io.javalin.security.RouteRole; import java.util.Arrays; -public class ServerValue extends MapValue { +class ServerValue extends MapValue { private final Javalin server; public ServerValue(Javalin server) { - super(10); + super(12); this.server = server; init(); } @@ -25,7 +26,9 @@ public class ServerValue extends MapValue { set("delete", httpMethod(server::delete)); set("options", httpMethod(server::options)); set("error", this::error); + set("exception", this::exception); set("start", this::start); + set("stop", this::stop); } private Value error(Value[] args) { @@ -45,6 +48,23 @@ public class ServerValue extends MapValue { return this; } + @SuppressWarnings("unchecked") + private Value exception(Value[] args) { + Arguments.check(2, args.length); + try { + String className = args[0].asString(); + final Class clazz = Class.forName(className); + final Function handler = ValueUtils.consumeFunction(args[1], 1); + server.exception((Class) clazz, (exc, ctx) -> { + Value exceptionType = new StringValue(exc.getClass().getName()); + handler.execute(exceptionType, new ContextValue(ctx)); + }); + } catch (ClassNotFoundException e) { + throw new OwnLangRuntimeException(e); + } + return this; + } + private Value start(Value[] args) { Arguments.checkRange(0, 2, args.length); switch (args.length) { @@ -55,6 +75,12 @@ public class ServerValue extends MapValue { return this; } + private Value stop(Value[] args) { + Arguments.check(0, args.length); + server.stop(); + return this; + } + private FunctionValue httpMethod(HttpMethodHandler httpHandler) { return new FunctionValue(args -> { Arguments.checkAtLeast(2, args.length); diff --git a/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java b/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java index fd508b9..514369b 100644 --- a/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java +++ b/modules/server/src/main/java/com/annimon/ownlang/modules/server/server.java @@ -3,6 +3,7 @@ package com.annimon.ownlang.modules.server; import com.annimon.ownlang.lib.*; import com.annimon.ownlang.modules.Module; import io.javalin.Javalin; +import io.javalin.http.staticfiles.Location; import java.util.Map; import static java.util.Map.entry; @@ -26,15 +27,19 @@ public final class server implements Module { if (args.length == 0) { return new ServerValue(Javalin.create()); } else { - final Function configConsumer = ValueUtils.consumeFunction(args[0], 0); - return new ServerValue(Javalin.create(config -> configConsumer.execute())); + final Map map = ValueUtils.consumeMap(args[0], 0).getMapStringKeys(); + final Config config = new Config(map); + return new ServerValue(Javalin.create(config::setup)); } } private Value serve(Value[] args) { - // get path, port - // javalin start() - return NumberValue.ZERO; - + Arguments.checkRange(0, 2, args.length); + int port = args.length >= 1 ? args[0].asInt() : 8080; + String dir = args.length >= 2 ? args[1].asString() : "."; + return new ServerValue(Javalin.create(config -> { + config.staticFiles.add(dir, Location.EXTERNAL); + config.showJavalinBanner = false; + }).start(port)); } } diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/lib/Converters.java b/ownlang-core/src/main/java/com/annimon/ownlang/lib/Converters.java index 9415df0..32934ac 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/lib/Converters.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/lib/Converters.java @@ -1,6 +1,7 @@ package com.annimon.ownlang.lib; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import static com.annimon.ownlang.lib.ValueUtils.getFloatNumber; /** @@ -273,4 +274,11 @@ public final class Converters { return NumberValue.fromBoolean(f.test(args[0].asString())); }); } + + public static FunctionValue stringToString(UnaryOperator f) { + return new FunctionValue(args -> { + Arguments.check(1, args.length); + return new StringValue(f.apply(args[0].asString())); + }); + } } diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/lib/MapValue.java b/ownlang-core/src/main/java/com/annimon/ownlang/lib/MapValue.java index 6fecbcf..eedac0d 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/lib/MapValue.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/lib/MapValue.java @@ -85,6 +85,17 @@ public class MapValue implements Value, Iterable> { public Map getMap() { return map; } + + public Map getMapStringKeys() { + return convertMap(Value::asString, java.util.function.Function.identity()); + } + + public Map convertMap(java.util.function.Function keyMapper, + java.util.function.Function valueMapper) { + final Map result = new LinkedHashMap<>(map.size()); + map.forEach((key, value) -> result.put(keyMapper.apply(key), valueMapper.apply(value))); + return result; + } @Override public Object raw() { diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/lib/ValueUtils.java b/ownlang-core/src/main/java/com/annimon/ownlang/lib/ValueUtils.java index 46c3dd9..3b74a10 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/lib/ValueUtils.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/lib/ValueUtils.java @@ -101,6 +101,15 @@ public final class ValueUtils { return result; } + public static MapValue consumeMap(Value value, int argumentNumber) { + final int type = value.type(); + if (type != Types.MAP) { + throw new TypeException("Map expected at argument " + (argumentNumber + 1) + + ", but found " + Types.typeToString(type)); + } + return (MapValue) value; + } + public static Function consumeFunction(Value value, int argumentNumber) { return consumeFunction(value, " at argument " + (argumentNumber + 1)); }