From ad00799a29084b16c05bbc6f78c186947b46054e Mon Sep 17 00:00:00 2001 From: Janos Erdos Date: Tue, 30 Jul 2024 21:55:59 +0200 Subject: [PATCH] feat: functions provider SPI (#166) --- build.clj | 11 ++++++- docs/Functions.md | 11 ++++++- .../stencil/functions/BasicFunctions.java | 8 +++++ .../stencil/functions/DateFunctions.java | 7 +++++ .../stencil/functions/FunctionEvaluator.java | 31 +++++++++++++------ .../stencil/functions/FunctionsProvider.java | 5 +++ .../stencil/functions/LocaleFunctions.java | 8 +++++ .../stencil/functions/NumberFunctions.java | 9 ++++++ .../stencil/functions/StringFunctions.java | 7 +++++ 9 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 java-src/io/github/erdos/stencil/functions/FunctionsProvider.java diff --git a/build.clj b/build.clj index 7e699dc6..fe385089 100644 --- a/build.clj +++ b/build.clj @@ -1,5 +1,6 @@ (ns build - (:require [clojure.tools.build.api :as b] + (:require [clojure.java.io :as io] + [clojure.tools.build.api :as b] [clojure.tools.build.util.file :as file])) (def build-folder "target") @@ -29,6 +30,14 @@ :javac-opts ["-source" "8" "-target" "8"]}) (b/copy-file {:src "java-src/io/github/erdos/stencil/standalone/help.txt" :target "target/classes/io/github/erdos/stencil/standalone/help.txt"}) + + ;; generate service provider config for function providers + (let [services (io/file "target/classes/META-INF/services/io.github.erdos.stencil.functions.FunctionsProvider")] + (io/make-parents services) + (doseq [f (file-seq (clojure.java.io/file jar-content)) + [_ a] (re-seq #"^target/classes/(.*\$Provider).class$" (str f)) + :let [clazz (str (.replace a "/" ".") "\n")]] + (spit services, clazz :append true))) (spit (str jar-content "/stencil-version") version) opts) diff --git a/docs/Functions.md b/docs/Functions.md index 60df5531..38bb7b75 100644 --- a/docs/Functions.md +++ b/docs/Functions.md @@ -233,7 +233,9 @@ Expects one number argument containing a list with numbers. Sums up the numbers ## Custom functions -You can register custom implementations of `io.github.erdos.stencil.functions.Function` or the `stencil.functions/call-fn` multimethod. +You can register custom implementations of `io.github.erdos.stencil.functions.Function` or the `stencil.functions/call-fn` multimethod. +If you implement the `call-fn` multimethod, the namespace containing these implementations should be loaded before rendering a document. +(Keep in mind, that `call-fn` implementations always have priority over `io.github.erdos.stencil.functions.Function` implementations) Clojure example: @@ -271,3 +273,10 @@ public class FirstFuncion implements Function { API.render(preparedTemplate, fragments, data, Arrays.asList(new FirstFunction())); ``` + + +### Automatic registration of custom functions + +Stencil uses the JVM's [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) facility to load function provider implementations. +If you want to register your custom functions automatically, implement the `io.github.erdos.stencil.functions.FunctionsProvider` interface, +and add these implementations to your extension library's `META-INF/services/io.github.erdos.stencil.functions.FunctionsProvider` file. \ No newline at end of file diff --git a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java index cfb8bc3f..c76c9c19 100644 --- a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java @@ -1,5 +1,6 @@ package io.github.erdos.stencil.functions; +import java.util.Arrays; import java.util.Collection; /** @@ -72,4 +73,11 @@ public Object call(Object... arguments) { public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionsProvider { + @Override + public Iterable functions() { + return Arrays.asList(values()); + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/DateFunctions.java b/java-src/io/github/erdos/stencil/functions/DateFunctions.java index 2ce892cc..7d748080 100644 --- a/java-src/io/github/erdos/stencil/functions/DateFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/DateFunctions.java @@ -136,4 +136,11 @@ private static Optional maybeLocalDateTime(Object obj) { public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionsProvider { + @Override + public Iterable functions() { + return asList(values()); + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java b/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java index b74e49b4..4e8d3648 100644 --- a/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java +++ b/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java @@ -3,38 +3,49 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.ServiceLoader; public final class FunctionEvaluator { + private final static Map preloaded; - private final Map functions = new HashMap<>(); + static { + preloaded = new HashMap<>(); + for (FunctionsProvider provider : ServiceLoader.load(FunctionsProvider.class)) { + for (Function fn : provider.functions()) { + registerFunction(preloaded, fn); + } + } + } + + private final Map functions; { - registerFunctions(BasicFunctions.values()); - registerFunctions(StringFunctions.values()); - registerFunctions(NumberFunctions.values()); - registerFunctions(DateFunctions.values()); - registerFunctions(LocaleFunctions.values()); + this.functions = new HashMap<>(preloaded); } - private void registerFunction(Function function) { + private static void registerFunction(Map map, Function function) { if (function == null) throw new IllegalArgumentException("Registered function must not be null."); - functions.put(function.getName().toLowerCase(), function); + Function present = map.put(function.getName().toLowerCase(), function); + if (present != null) + throw new IllegalArgumentException("Function with name has already been registered."); } /** * Registers a function to this evaluator engine. * Registered functions can be invoked from inside template files. * - * @param functions any number of function instances. + * @param functions list of functions to register */ + @SuppressWarnings("WeakerAccess") public void registerFunctions(Function... functions) { for (Function function : functions) { - registerFunction(function); + registerFunction(this.functions, function); } } + /** * Calls a function by name. * diff --git a/java-src/io/github/erdos/stencil/functions/FunctionsProvider.java b/java-src/io/github/erdos/stencil/functions/FunctionsProvider.java new file mode 100644 index 00000000..44d8e585 --- /dev/null +++ b/java-src/io/github/erdos/stencil/functions/FunctionsProvider.java @@ -0,0 +1,5 @@ +package io.github.erdos.stencil.functions; + +public interface FunctionsProvider { + Iterable functions(); +} diff --git a/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java b/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java index fbf14e5a..327160f0 100644 --- a/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java @@ -1,6 +1,7 @@ package io.github.erdos.stencil.functions; import java.text.NumberFormat; +import java.util.Arrays; import java.util.Locale; import static java.util.Locale.forLanguageTag; @@ -53,4 +54,11 @@ private static String formatting(Function function, java.util.function.Function< public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionsProvider { + @Override + public Iterable functions() { + return Arrays.asList(values()); + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/NumberFunctions.java b/java-src/io/github/erdos/stencil/functions/NumberFunctions.java index b44207ba..a1ce7bab 100644 --- a/java-src/io/github/erdos/stencil/functions/NumberFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/NumberFunctions.java @@ -1,5 +1,7 @@ package io.github.erdos.stencil.functions; +import java.util.Arrays; + /** * Common numeric functions. */ @@ -73,4 +75,11 @@ private static Number maybeNumber(Object... arguments) { return (Number) arguments[0]; } } + + public static class Provider implements FunctionsProvider { + @Override + public Iterable functions() { + return Arrays.asList(values()); + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/StringFunctions.java b/java-src/io/github/erdos/stencil/functions/StringFunctions.java index ca2bb6d8..87e9f96e 100644 --- a/java-src/io/github/erdos/stencil/functions/StringFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/StringFunctions.java @@ -112,4 +112,11 @@ public Object call(Object... arguments) throws IllegalArgumentException { public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionsProvider { + @Override + public Iterable functions() { + return Arrays.asList(values()); + } + } }