User Defined Functions¶
Users may define and call custom functions of their own in REP script. Currently, Juji supports user defined functions (UDF) written in programming languages running on Java Virtual Machine (JVM), such as Java, Clojure, Scala, Kotlin and so on, as long as they can be called through Clojure's Java interop syntax.
Juji does not make a lot of assumptions about what your UDFs do, except that these functions will be used similarly as REP system functions in REP script, i.e. as Boolean functions in trigger pattern and as output functions in action pattern.
Different engagement's UDF run in different isolated sandbox to ensure security
and privacy. Exceptions thrown in UDFs are logged and the thowing UDF will
return nil
in these cases.
JVM UDF¶
User can write code in JVM languages and package the compiled classes into a JAR file, e.g. using standard package management tools such as Maven. User then upload the JAR file to Juji, so that the JAR is deployed to a secure private Maven repository for the user's REP script to access.
Upload JAR¶
In IDE file browser, click open the desired engagement, click open Config
file, on the menu bar of the main content panel, you will see a Add Jar
button.
Click the button to see a dialog for uploading the JAR file.
The JAR file field is required and other fields are optional. However, groupId
(organization identifier), artifactId
(package identifier) and version
are all required information for deployment to the private Maven
repository. You can supply a pom.xml
file that contains these entries, or Juji
will attempt to find the pom.xml
file inside the JAR file, or you can fill in
the form.
JAR without dependencies¶
To reduce the size of the uploaded file, it is recommended to upload JAR files that
only contain your own classes. The dependencies should be described in a
pom.xml
file that is inside the JAR or is uploaded along with the JAR. Juji will
automatically resolve these dependencies if they are available on Maven
Central or clojars.
JAR with dependencies¶
If the package uses libraries that are not publicly available on Maven Central or clojars, these libraries should be included in the uploaded JAR file, e.g. by using tools such as maven-assembly-plugin.
Import Dependencies¶
Follow these two steps to import dependencies into your REP script, whether the dependency is on a uploaded JAR file or on an artifact available on Maven Central or clojars public repositories.
Add :dependencies
in config
form¶
First the REP script needs to tell the compiler that there are external
dependencies. The dependencies should be declared in the config
form of the
script, under :dependencies
key.
When the script is compiled by clicking the Compile
button, the referred
dependencies will be fetched from remote repositories and add to the class path.
A dedicated Java class loader will then be created to load the dependencies.
Uploaded dependency¶
After a JAR file is successfully uploaded, the Chat Config
document will be
automatically updated to include the dependencies under a :dependencies
key. The
value is a vector of dependency coordinates, and the individual dependency
coordinate has this format: [<groupId>/<artifactId> <version>]
;; Chat Config document
{...
:dependencies [["io.juji/java-test" "1.2.2"]
["com.mycorp/mylib" "2.3.1"]]}
Click Generate Script
drop down menu, and select either Preview
or Web
releases, a REP script will be generated from the config document. Find the
generated script in file browser, click open the file, you will see that the
dependency coordinates have been copied to the config
form of the script, also
under :dependencies
key.
;; REP script
(config {...
:dependencies [[io.juji/java-test "1.2.2"]
[com.mycorp/mylib "2.3.1"]]})
Note
There is a slight difference in the format of the dependencies value:
groupId/artifactId
in REP script is not quoted.
Public dependency¶
If you know the coordinate of a publicly available library on Maven Central or
clojars, you can add it to the :dependencies
key manually in the config
section of the REP script, just like the auto-generated ones above.
Add :import
in ns
form¶
The second step is to tell the REP runtime which classes should be imported into
the script namespace, so that these classes can be used by UDFs. To do that, you
need to modify the ns
form of the script to include a (:import
[<class1 path>] [<class2 path>] ...)
clause, e.g.
(ns juji.eng1.kaya
(:import [io.juji.javatest Client]
[com.mycorp.mylib AwesomeClass UsefulClass]))
io.juji.javatest.Client
,
com.mycorp.mylib.AwesomeClass
and com.mycorp.mylib.UsefulClass
.
Call Function via Java Interop¶
Once the classes are imported, standard Clojure-Java interop syntax can be used in REP script to call JVM UDF, e.g.
(AClass/aClassMethod "argument") ; call class method
(doto (AClass. "constructorArgument") ; create new object with constructor
(.instanceMethodA "argument1") ; invoke instance method on object
(.instanceMethodB)) ; invoke another method on the same object
As you can see, this is a very concise way of calling Java.
Clojure UDF¶
Juji is developed in Clojure, a powerful general purpose programming language. Juji users can harness the most of the same power in REP script as well, by directly writing Clojure functions in REP scripts using the standard Clojure function syntax.
(ns com.mycorp.eng2.kaya
(:require [java-time :as time])) ; this is a Clojure library
(defn first-monday-of-month
"Define a UDF that returns the date object of the first Monday
of this month"
[]
(time/adjust (time/local-date) :first-in-month :monday))
(deftopic first-monday-month []
[you know first monday month]
["I know. It is" (time/format "MM/dd" (first-monday-of-month))])
(config {...
:dependencies [[clojure.java-time "0.3.2"]]
...})
This code requires a Clojure library called java-time
. Instead of using
the :import
keyword for Java classes, :require
should be used for Clojure
libraries.
The UDF is defined using a defn
form, a standard way of creating new function
in Clojure. The function is called in the topic as part of the action.
We also added the coordinate of the library to :dependencies
key of the
config
map. Since this library is publicly available on clojars, Juji will
fetch the library and put it on classpath during script compilation.
Sandbox Restrictions¶
Obviously, not everything is allowed to run in a UDF, or one could break out of the sandbox to compromise the security and privacy of everyone else on the system.
Most of the JVM system level operations, reflection, GC and so on are not permitted to run in UDFs.
Clojure dynamic functionalities such as code evaluation, namespace manipulation, try-catch exception handling, and so on, are not permitted.
UDFs do have access to the Internet, but is not permitted to serve external requests.
Other Languages¶
For custom functionalities written in non-JVM languages, it is possible to
access them via HTTP requests using make-request
system
function if they are exposed as REST APIs. If they are accessible
via other protocols, you can write JVM based UDFs to implement these protocols.
Or if you prefer to write your own program in whatever languages and run your own application on your own platform, e.g. on mobile devices, we have a suite of GraphQL API that you can use to access Juji platform remotely, as long as your program can access Juji through HTTPS and can talk GraphQL over Websocket.