Skip to content

Commit

Permalink
Closes #82: Added ability for different functions/methods to be regis…
Browse files Browse the repository at this point in the history
…tered against different JactlContext objects
  • Loading branch information
jaccomoc committed Jan 1, 2025
1 parent d7bfe90 commit b10f420
Show file tree
Hide file tree
Showing 18 changed files with 506 additions and 204 deletions.
111 changes: 103 additions & 8 deletions docs/pages/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,21 @@ By default, classes are not allowed to access globals and access will result in
If for your application it makes sense for classes to have access to globals then you can invoke this method
with `true`.

### hasOwnFunctions(boolean value)

This controls whether the `JactlContext` object will have its own set of functions/methods registered with it.
By default, all `JactlContext` share the same functions/methods registered using `Jactl.function() ... .register()`
or `Jactl.method(type) ... .register()` (see section below on [Adding New Functions/Methods](#adding-new-functionsmethods)).

If you would like to have different sets of functions/methods for different sets of scripts you can create different
`JactlContext` objects and register different sets of functions/methods with each object.

Note that whatever functions/methods have been registered at the time that the `JactlContext` is created will be
available to scripts compiled with that `JactlContext` so it makes sense to register all functions/methods that you
would like to be available to all scripts before creating any `JactlContext` objects.

See [Adding New Functions/Methods](#adding-new-functionsmethods) for more details.

### Chaining Method Calls

The methods for building a `JactlContext` can be chained in any order (apart from `create()` which must be first
Expand All @@ -331,11 +346,12 @@ JactlContext context = JactlContext.create()
.environment(new io.jactl.DefaultEnv())
.minScale(10)
.classAccessToGlobals(false)
.hasOwnFunctions(false)
.debug(0)
.build();

// This is equivalent to:
JactlContext context = JactlContext.create().build()
JactlContext context = JactlContext.create().build();
```

## Compiling Classes
Expand Down Expand Up @@ -996,9 +1012,9 @@ This allows you to configure the class name in your `.jactlrc` file and have the
in the REPL and in commandline scripts.

For example:
```groovy
```java
class MyFunctions {
public static registerFunctions(JactlEnv env) {
public static void registerFunctions(JactlEnv env) {
Jactl.method(JactlType.ANY)
.name("toJson")
.impl(JsonFunctions.class, "toJson")
Expand Down Expand Up @@ -1058,7 +1074,7 @@ class MyFunctions {

### Async Instance

For methods that act on `JacsalType.ITERATOR` objects, we allow the object to be one of the following types:
For methods that act on `JactlType.ITERATOR` objects, we allow the object to be one of the following types:
* List
* Map
* String — iterates of the characters of the string
Expand Down Expand Up @@ -1105,7 +1121,7 @@ do more processing when they are resumed.
A naive implementation of `measure()` might look like this:
```groovy
class MyFunctions {
public static void registerFunctions(JacsalEnv env) {
public static void registerFunctions(JactlEnv env) {
Jactl.function()
.name("measure")
.param("closure")
Expand Down Expand Up @@ -1239,7 +1255,7 @@ We also need to add a `Continuation` parameter to our function since it is now p
Putting this all together, our class now looks like this:
```java
class MyFunctions {
public static void registerFunctions(JacsalEnv env) {
public static void registerFunctions(JactlEnv env) {
Jactl.function()
.name("measure")
.asyncParam("closure")
Expand Down Expand Up @@ -1291,7 +1307,7 @@ In order for the resume method to invoke the original method, it will need to be
Now our code looks like this:
```java
class MyFunctions {
public static void registerFunctions(JacsalEnv env) {
public static void registerFunctions(JactlEnv env) {
Jactl.function()
.name("measure")
.param("count", 1)
Expand Down Expand Up @@ -1362,9 +1378,88 @@ Function@727860268
284375954
```

### Registering Functions/Methods for Specific `JactlContext` Objects

In all examples so far, the custom functions/methods that have created have been registered globally using `Jactl.function()`
and `Jactl.method(type)` and are therefore available to all scripts within the application.

If different sets of scripts should have access to different sets of functions/methods, then instead of using `Jactl.function()`
and `Jactl.method(type)` to register the function/method, you can create your `JactlContext` object and use the `function()`
and `method(type)` methods on it to register functions and methods that will only be visible to scripts compiled with
that `JactlContext`.

For example:
```java
class MyModule {

private static JactlContext context;

public static void registerFunctions(JactlContext context) {
context.method(JactlType.ANY)
.name("toJson")
.impl(JsonFunctions.class, "toJson")
.register();

context.method(JactlType.STRING)
.name("fromJson")
.impl(JsonFunctions.class, "fromJson")
.register();

context.function()
.name("getState")
.param("sessionId")
.impl(MyModule.class, "getState")
.register();
}

public static Object getStateData;
public static Map getState(long sessionId) { ... }

public void init(JactlEnv env) {
context = JactlContext.create()
.environment(env)
.hasOwnFunctions(true)
.build();

registerFunctions(context);
}

...
}
```

The way in which the function/method is registered is identical, except that we use the `JactlContext` object rather
than the `Jactl` class (as shown in the example).

Note that the `JactlContext` will also have access to all functions/methods that have already been registered using
`Jactl.function()` or `Jactl.method()` at the point at which the `JactlContext` is created.
If other functions/methods are later registered using `Jactl.function()` or `Jactl.method()` after the `JactlContext`
was created, these additional functions/methods will not be available to scripts compiled with that `JactlContext`.

### Deregistering Functions

It is possible to deregister a function/method so that it is no longer available to any new scripts that are compiled.
This might be useful in unit tests, for example.

To deregister a global function just pass the function name to `Jactl.deregister()`:
```java
Jactl.deregister("myFunction");
```

To deregister a function from a `JactlContext`:
```java
jactlContext.deregister("myFunction");
```

To deregister a method:
```java
Jactl.deregister(JactlType.STRING, "lines");
jactlContext.deregister(JactlType.LIST, "myListMethod");
```

## Example Application

In the `jacsal-vertx` project, an example application is provided that listens for JSON based web requests and
In the `Jactl-vertx` project, an example application is provided that listens for JSON based web requests and
runs a Jactl script based on the URI present in the request.

See [Example Application](https://github.com/jaccomoc/jactl-vertx#example-application) for more details.
4 changes: 2 additions & 2 deletions src/main/java/io/jactl/Jactl.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,15 @@ public static JactlFunction method(JactlType type) {
* @param name the name of the method
*/
public static void deregister(JactlType type, String name) {
BuiltinFunctions.deregisterFunction(type, name);
Functions.INSTANCE.deregisterFunction(type, name);
}

/**
* Deregister a global function
* @param name the name of the global function
*/
public static void deregister(String name) {
BuiltinFunctions.deregisterFunction(name);
Functions.INSTANCE.deregisterFunction(name);
}

////////////////////////////////////////////
Expand Down
91 changes: 79 additions & 12 deletions src/main/java/io/jactl/JactlContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package io.jactl;

import io.jactl.compiler.Compiler;
import io.jactl.runtime.*;

import java.io.File;
Expand All @@ -26,6 +27,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;

public class JactlContext {

Expand All @@ -39,6 +41,16 @@ public class JactlContext {
public boolean classAccessToGlobals = false; // Whether to allow class methods to access globals
public boolean checkClasses = false; // Whether to run CheckClassAdapter on generated byte code to check for errors (slowish)

private final Map<String, Function<Map<String,Object>,Object>> evalScriptCache = Collections.synchronizedMap(
new LinkedHashMap(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > scriptCacheSize;
}
});

public static int scriptCacheSize = 100; // Total number of compiled scripts we keep for use by eval() function

// Testing
boolean checkpoint = false;
boolean restore = false;
Expand Down Expand Up @@ -70,6 +82,8 @@ public class JactlContext {
private boolean isIdePlugin = false;
private File buildDir;

private Functions functions;

DynamicClassLoader classLoader = new DynamicClassLoader();

///////////////////////////////
Expand All @@ -80,18 +94,18 @@ public static JactlContextBuilder create() {

private JactlContext() {}

/**
* Get the current context by finding our thread current class loader.
* @return the current context
* @throws IllegalStateException if current class loader is not of correct type
*/
public static JactlContext getContext() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader instanceof DynamicClassLoader) {
return ((DynamicClassLoader)loader).getJactlContext();
}
throw new IllegalStateException("Expected class loader of type " + DynamicClassLoader.class.getName() + " but found " + loader.getClass().getName());
}
// /**
// * Get the current context by finding our thread current class loader.
// * @return the current context
// * @throws IllegalStateException if current class loader is not of correct type
// */
// public static JactlContext getContext() {
// ClassLoader loader = Thread.currentThread().getContextClassLoader();
// if (loader instanceof DynamicClassLoader) {
// return ((DynamicClassLoader)loader).getJactlContext();
// }
// throw new IllegalStateException("Expected class loader of type " + DynamicClassLoader.class.getName() + " but found " + loader.getClass().getName());
// }

/**
* Lookup class based on fully qualified internal name (a/b/c/X$Y)
Expand Down Expand Up @@ -157,6 +171,7 @@ public class JactlContextBuilder {
private JactlContextBuilder() {}

public JactlContextBuilder environment(JactlEnv env) { executionEnv = env; return this; }
public JactlContextBuilder hasOwnFunctions(boolean value) { functions = value ? new Functions(Functions.INSTANCE) : null; return this; }
public JactlContextBuilder minScale(int scale) { minScale = scale; return this; }
public JactlContextBuilder javaPackage(String pkg) { javaPackage = pkg; return this; }
public JactlContextBuilder classAccessToGlobals(boolean accessAllowed) {
Expand Down Expand Up @@ -207,6 +222,58 @@ public JactlContext build() {

//////////////////////////////////

public Functions getFunctions() {
return functions == null ? Functions.INSTANCE : functions;
}

public JactlFunction function() {
if (functions == null) {
throw new IllegalStateException("JactlContext was not built to have separate functions (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
}
return new JactlFunction(this);
}

public JactlFunction method(JactlType methodClass) {
if (functions == null) {
throw new IllegalStateException("JactlContext was not built to have separate methods (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
}
return new JactlFunction(this, methodClass);
}

public void deregister(String name) {
if (functions == null) {
throw new IllegalStateException("JactlContext was not built to have separate functions (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
}
getFunctions().deregisterFunction(name);
}

public void deregister(JactlType type, String name) {
if (functions == null) {
throw new IllegalStateException("JactlContext was not built to have separate methods (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
}
getFunctions().deregisterFunction(type, name);
}

public void clearScriptCache() {
evalScriptCache.clear();
}

public Function<Map<String, Object>, Object> getEvalScript(String code, Map bindings) {
Function<Map<String, Object>, Object> script = evalScriptCache.get(code);
if (script == null) {
// For eval we want to be able to cache the scripts but the problem is that if the bindings
// are typed (e.g. x is an Integer) but then when script is rerun a global has had its type
// changed (e.g. x is now a Long) the script will fail because the types don't match. So
// we erase all types and make everything ANY. This is obviously less efficient but for eval()
// efficiency should not be a big issue.
HashMap erasedBindings = new HashMap();
bindings.keySet().forEach(k -> erasedBindings.put(k, null));
script = Compiler.compileScriptInternal(code, this, Utils.DEFAULT_JACTL_PKG, erasedBindings);
evalScriptCache.put(code, script);
}
return script;
}

public boolean printLoop() { return printLoop; }
public boolean nonPrintLoop() { return nonPrintLoop; }

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/jactl/JactlScript.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private static void cleanUp(JactlScriptObject instance, JactlContext context) {
* @param completion code to be run once script finishes
*/
public void run(Map<String,Object> globals, BufferedReader input, PrintStream output, Consumer<Object> completion) {
RuntimeState.setState(globals, input, output);
RuntimeState.setState(jactlContext, globals, input, output);
script.accept(globals, completion);
}

Expand Down
9 changes: 8 additions & 1 deletion src/main/java/io/jactl/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,14 @@ public static Expr.FunDecl createWrapperFunDecl(Token token, String name, boolea
}

public static Method findStaticMethod(Class clss, String methodName) {
return findMethod(clss, methodName, true);
if (clss == null) {
throw new IllegalArgumentException("Implementation class not specified");
}
Method method = findMethod(clss, methodName, true);
if (method == null) {
throw new IllegalArgumentException("Could not find method " + methodName + " in " + clss.getName());
}
return method;
}

public static Method findMethod(Class clss, String methodName, boolean isStatic) {
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/io/jactl/compiler/MethodCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,10 @@ void compileRegexMatch(Expr.RegexMatch expr, Runnable compileString) {
else {
// We need to find the method handle for the given method
FunctionDescriptor method = clss.getMethod(name);
if (method == null) {
// Look for builtin method
method = classCompiler.context.getFunctions().lookupMethod(clss.getInstanceType(), name);
}
check(method != null, "could not find method or field called " + name + " for " + expr.left.type);
// We want the handle to the wrapper method.
loadWrapperHandle(method, expr);
Expand Down Expand Up @@ -1787,7 +1791,7 @@ private void loadIndexField(Expr.Binary expr, JactlType parentType) {
if (isBuiltinFunction) {
// If we have the name of a built-in function then lookup its method handle
loadConst(name);
invokeMethod(BuiltinFunctions.class, "lookupMethodHandle", String.class);
invokeMethod(RuntimeUtils.class, RuntimeUtils.LOOKUP_METHOD_HANDLE, String.class);
}
else
if (name.charAt(0) == '$' && Utils.isDigits(name.substring(1))) {
Expand Down
Loading

0 comments on commit b10f420

Please sign in to comment.