diff --git a/docs/Functions.md b/docs/Functions.md index 60df5531..cce3ae15 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,8 @@ 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.FunctionProvider` interface, and add these implementations to your extension library's `META-INF/services/io.github.erdos.stencil.functions.FunctionProvider` file. +The `call-fn` implementations which are defined in your namespaces can also be loaded using this facility, if you override `io.github.erdos.stencil.functions.ClojureCallFnProvider` abstract class, and register it as a `FunctionProvider`. See `io.github.erdos.stencil.functions.DefaultCallFnProvider` as an example. diff --git a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java index cfb8bc3f..043b6f3b 100644 --- a/java-src/io/github/erdos/stencil/functions/BasicFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/BasicFunctions.java @@ -1,6 +1,8 @@ package io.github.erdos.stencil.functions; +import java.util.Arrays; import java.util.Collection; +import java.util.List; /** * Common general purpose functions. @@ -72,4 +74,14 @@ public Object call(Object... arguments) { public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionProvider { + + private static final List FUNCTIONS = Arrays.asList(values()); + + @Override + public Collection functions() { + return FUNCTIONS; + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/ClojureCallFnProvider.java b/java-src/io/github/erdos/stencil/functions/ClojureCallFnProvider.java new file mode 100644 index 00000000..d7be9933 --- /dev/null +++ b/java-src/io/github/erdos/stencil/functions/ClojureCallFnProvider.java @@ -0,0 +1,40 @@ +package io.github.erdos.stencil.functions; + +import io.github.erdos.stencil.impl.ClojureHelper; + +import java.util.Collection; +import java.util.Collections; + +/** + * This provider requires a Clojure NS which contains implementations of the call-fn multimethod. + * The constructor defines the + */ +public abstract class ClojureCallFnProvider implements FunctionProvider { + + /** + * The constructor loads the Clojure namespace which contains the call-fn multimethod implementations. + * This should be called from the no-args constructor of the concrete class. + * @param ns the clojuse namespace which contains the function implementations. + */ + protected ClojureCallFnProvider(final String ns) { + ClojureHelper.requireNs(ns); + } + + @Override + /** + * This provider does not provide any functions directly. + * + * @return an empty collection + */ + public Collection functions() { + return Collections.emptyList(); + } + + @Override + /* + * call-fn implementations have priority over Java Function implementations during evaluation. + */ + public int priority() { + return Integer.MIN_VALUE; + } +} diff --git a/java-src/io/github/erdos/stencil/functions/DateFunctions.java b/java-src/io/github/erdos/stencil/functions/DateFunctions.java index 2ce892cc..ebc0cb60 100644 --- a/java-src/io/github/erdos/stencil/functions/DateFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/DateFunctions.java @@ -8,10 +8,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Optional; +import java.util.*; import static java.util.Arrays.asList; import static java.util.Locale.forLanguageTag; @@ -136,4 +133,14 @@ private static Optional maybeLocalDateTime(Object obj) { public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionProvider { + + private static final List FUNCTIONS = Arrays.asList(values()); + + @Override + public Collection functions() { + return FUNCTIONS; + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/DefaultCallFnProvider.java b/java-src/io/github/erdos/stencil/functions/DefaultCallFnProvider.java new file mode 100644 index 00000000..3a40e775 --- /dev/null +++ b/java-src/io/github/erdos/stencil/functions/DefaultCallFnProvider.java @@ -0,0 +1,11 @@ +package io.github.erdos.stencil.functions; + +public class DefaultCallFnProvider extends ClojureCallFnProvider { + /** + * The built-in call-fn functions are defined in the stencil.functions namespace. + * This constructor loads it. + */ + public DefaultCallFnProvider() { + super("stencil.functions"); + } +} diff --git a/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java b/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java index b74e49b4..6bdebbc7 100644 --- a/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java +++ b/java-src/io/github/erdos/stencil/functions/FunctionEvaluator.java @@ -1,6 +1,7 @@ package io.github.erdos.stencil.functions; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -8,12 +9,14 @@ public final class FunctionEvaluator { private final Map functions = new HashMap<>(); - { - registerFunctions(BasicFunctions.values()); - registerFunctions(StringFunctions.values()); - registerFunctions(NumberFunctions.values()); - registerFunctions(DateFunctions.values()); - registerFunctions(LocaleFunctions.values()); + public FunctionEvaluator() { + this(FunctionLoader.getFunctions()); + } + + public FunctionEvaluator(Collection functions) { + for (Function f : functions) { + registerFunction(f); + } } private void registerFunction(Function function) { diff --git a/java-src/io/github/erdos/stencil/functions/FunctionLoader.java b/java-src/io/github/erdos/stencil/functions/FunctionLoader.java new file mode 100644 index 00000000..c14d0c2c --- /dev/null +++ b/java-src/io/github/erdos/stencil/functions/FunctionLoader.java @@ -0,0 +1,20 @@ +package io.github.erdos.stencil.functions; + +import java.util.Comparator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +final class FunctionLoader { + private FunctionLoader() {} + + private static final ServiceLoader REGISTRARS = ServiceLoader.load(FunctionProvider.class); + + static List getFunctions() { + return StreamSupport.stream(REGISTRARS.spliterator(), false) + .sorted(Comparator.comparingInt(FunctionProvider::priority)) + .flatMap(p -> p.functions().stream()) + .collect(Collectors.toList()); + } +} diff --git a/java-src/io/github/erdos/stencil/functions/FunctionProvider.java b/java-src/io/github/erdos/stencil/functions/FunctionProvider.java new file mode 100644 index 00000000..701ad81f --- /dev/null +++ b/java-src/io/github/erdos/stencil/functions/FunctionProvider.java @@ -0,0 +1,32 @@ +package io.github.erdos.stencil.functions; + +import java.util.Collection; + +public interface FunctionProvider { + + + int DEFAULT_PRIORITY = 10; + + /** + * Return the functions instances for the current render. + * The provider may choose to return new instances of a function for each call, if the function is not pure: + * e.g.: a counter, which returns an increasing sequence of numbers for each call. + * + * @return the functions provided by this provider + */ + Collection functions(); + + /** + * Priority of the provider. + *

+ * Providers are called in ascending order of priority. + *

+ * Default priority is 10. + * NB: Multimethod functions defined in Clojure namespaces have priority over Java defined functions. + * + * @return priority of the provider + */ + default int priority() { + return DEFAULT_PRIORITY; + } +} diff --git a/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java b/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java index fbf14e5a..296bc0a8 100644 --- a/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/LocaleFunctions.java @@ -1,6 +1,9 @@ package io.github.erdos.stencil.functions; import java.text.NumberFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Locale; import static java.util.Locale.forLanguageTag; @@ -53,4 +56,14 @@ private static String formatting(Function function, java.util.function.Function< public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionProvider { + + private static final List FUNCTIONS = Arrays.asList(values()); + + @Override + public Collection functions() { + return FUNCTIONS; + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/NumberFunctions.java b/java-src/io/github/erdos/stencil/functions/NumberFunctions.java index b44207ba..bf2f01a1 100644 --- a/java-src/io/github/erdos/stencil/functions/NumberFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/NumberFunctions.java @@ -1,5 +1,9 @@ package io.github.erdos.stencil.functions; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + /** * Common numeric functions. */ @@ -73,4 +77,14 @@ private static Number maybeNumber(Object... arguments) { return (Number) arguments[0]; } } + + public static class Provider implements FunctionProvider { + + private static final List FUNCTIONS = Arrays.asList(values()); + + @Override + public Collection functions() { + return FUNCTIONS; + } + } } diff --git a/java-src/io/github/erdos/stencil/functions/StringFunctions.java b/java-src/io/github/erdos/stencil/functions/StringFunctions.java index ca2bb6d8..80e0a093 100644 --- a/java-src/io/github/erdos/stencil/functions/StringFunctions.java +++ b/java-src/io/github/erdos/stencil/functions/StringFunctions.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -112,4 +113,14 @@ public Object call(Object... arguments) throws IllegalArgumentException { public String getName() { return name().toLowerCase(); } + + public static class Provider implements FunctionProvider { + + private static final List FUNCTIONS = Arrays.asList(values()); + + @Override + public Collection functions() { + return FUNCTIONS; + } + } } diff --git a/java-src/io/github/erdos/stencil/impl/ClojureHelper.java b/java-src/io/github/erdos/stencil/impl/ClojureHelper.java index 12b39bca..e5c4abf7 100644 --- a/java-src/io/github/erdos/stencil/impl/ClojureHelper.java +++ b/java-src/io/github/erdos/stencil/impl/ClojureHelper.java @@ -4,6 +4,10 @@ import clojure.lang.RT; import clojure.lang.Symbol; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + /** * Clojure utilities. */ @@ -12,10 +16,27 @@ public final class ClojureHelper { private ClojureHelper() {} + enum Keywords { + DATA, FUNCTION, FRAGMENTS, TEMPLATE, VARIABLES, SOURCE_FOLDER, WRITER; + + public final Keyword kw = Keyword.intern(name().toLowerCase().replace('_', '-')); + + public final V getOrThrow(Map m) { + if (!m.containsKey(kw)) { + throw new IllegalArgumentException("Map does not contain keyword " + kw); + } else { + return m.get(kw); + } + } + } + + //Do not require namespace which is already loaded. + private static final Set ALREADY_REQUIRED_NAMESPACES; + // requires stencil.process namespace so stencil is loaded. static { - final IFn req = RT.var("clojure.core", "require"); - req.invoke(Symbol.intern("stencil.process")); + ALREADY_REQUIRED_NAMESPACES = new HashSet<>(); + requireNs("stencil.process"); } /** @@ -27,4 +48,15 @@ private ClojureHelper() {} public static IFn findFunction(String functionName) { return RT.var("stencil.process", functionName); } + + + public static void requireNs(String ns) { + final Symbol nsSym = Symbol.intern(ns); + if (ALREADY_REQUIRED_NAMESPACES.contains(nsSym)) { + return; + } + final IFn req = RT.var("clojure.core", "require"); + req.invoke(nsSym); + ALREADY_REQUIRED_NAMESPACES.add(nsSym); + } } diff --git a/resources/META-INF/services/io.github.erdos.stencil.functions.FunctionProvider b/resources/META-INF/services/io.github.erdos.stencil.functions.FunctionProvider new file mode 100644 index 00000000..acff3980 --- /dev/null +++ b/resources/META-INF/services/io.github.erdos.stencil.functions.FunctionProvider @@ -0,0 +1,6 @@ +io.github.erdos.stencil.functions.DefaultCallFnProvider +io.github.erdos.stencil.functions.BasicFunctions$Provider +io.github.erdos.stencil.functions.StringFunctions$Provider +io.github.erdos.stencil.functions.NumberFunctions$Provider +io.github.erdos.stencil.functions.DateFunctions$Provider +io.github.erdos.stencil.functions.LocaleFunctions$Provider