Skip to content

Commit b10f420

Browse files
committed
Closes #82: Added ability for different functions/methods to be registered against different JactlContext objects
1 parent d7bfe90 commit b10f420

18 files changed

+506
-204
lines changed

docs/pages/integration-guide.md

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,21 @@ By default, classes are not allowed to access globals and access will result in
320320
If for your application it makes sense for classes to have access to globals then you can invoke this method
321321
with `true`.
322322

323+
### hasOwnFunctions(boolean value)
324+
325+
This controls whether the `JactlContext` object will have its own set of functions/methods registered with it.
326+
By default, all `JactlContext` share the same functions/methods registered using `Jactl.function() ... .register()`
327+
or `Jactl.method(type) ... .register()` (see section below on [Adding New Functions/Methods](#adding-new-functionsmethods)).
328+
329+
If you would like to have different sets of functions/methods for different sets of scripts you can create different
330+
`JactlContext` objects and register different sets of functions/methods with each object.
331+
332+
Note that whatever functions/methods have been registered at the time that the `JactlContext` is created will be
333+
available to scripts compiled with that `JactlContext` so it makes sense to register all functions/methods that you
334+
would like to be available to all scripts before creating any `JactlContext` objects.
335+
336+
See [Adding New Functions/Methods](#adding-new-functionsmethods) for more details.
337+
323338
### Chaining Method Calls
324339

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

337353
// This is equivalent to:
338-
JactlContext context = JactlContext.create().build()
354+
JactlContext context = JactlContext.create().build();
339355
```
340356

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

9981014
For example:
999-
```groovy
1015+
```java
10001016
class MyFunctions {
1001-
public static registerFunctions(JactlEnv env) {
1017+
public static void registerFunctions(JactlEnv env) {
10021018
Jactl.method(JactlType.ANY)
10031019
.name("toJson")
10041020
.impl(JsonFunctions.class, "toJson")
@@ -1058,7 +1074,7 @@ class MyFunctions {
10581074

10591075
### Async Instance
10601076

1061-
For methods that act on `JacsalType.ITERATOR` objects, we allow the object to be one of the following types:
1077+
For methods that act on `JactlType.ITERATOR` objects, we allow the object to be one of the following types:
10621078
* List
10631079
* Map
10641080
* String — iterates of the characters of the string
@@ -1105,7 +1121,7 @@ do more processing when they are resumed.
11051121
A naive implementation of `measure()` might look like this:
11061122
```groovy
11071123
class MyFunctions {
1108-
public static void registerFunctions(JacsalEnv env) {
1124+
public static void registerFunctions(JactlEnv env) {
11091125
Jactl.function()
11101126
.name("measure")
11111127
.param("closure")
@@ -1239,7 +1255,7 @@ We also need to add a `Continuation` parameter to our function since it is now p
12391255
Putting this all together, our class now looks like this:
12401256
```java
12411257
class MyFunctions {
1242-
public static void registerFunctions(JacsalEnv env) {
1258+
public static void registerFunctions(JactlEnv env) {
12431259
Jactl.function()
12441260
.name("measure")
12451261
.asyncParam("closure")
@@ -1291,7 +1307,7 @@ In order for the resume method to invoke the original method, it will need to be
12911307
Now our code looks like this:
12921308
```java
12931309
class MyFunctions {
1294-
public static void registerFunctions(JacsalEnv env) {
1310+
public static void registerFunctions(JactlEnv env) {
12951311
Jactl.function()
12961312
.name("measure")
12971313
.param("count", 1)
@@ -1362,9 +1378,88 @@ Function@727860268
13621378
284375954
13631379
```
13641380

1381+
### Registering Functions/Methods for Specific `JactlContext` Objects
1382+
1383+
In all examples so far, the custom functions/methods that have created have been registered globally using `Jactl.function()`
1384+
and `Jactl.method(type)` and are therefore available to all scripts within the application.
1385+
1386+
If different sets of scripts should have access to different sets of functions/methods, then instead of using `Jactl.function()`
1387+
and `Jactl.method(type)` to register the function/method, you can create your `JactlContext` object and use the `function()`
1388+
and `method(type)` methods on it to register functions and methods that will only be visible to scripts compiled with
1389+
that `JactlContext`.
1390+
1391+
For example:
1392+
```java
1393+
class MyModule {
1394+
1395+
private static JactlContext context;
1396+
1397+
public static void registerFunctions(JactlContext context) {
1398+
context.method(JactlType.ANY)
1399+
.name("toJson")
1400+
.impl(JsonFunctions.class, "toJson")
1401+
.register();
1402+
1403+
context.method(JactlType.STRING)
1404+
.name("fromJson")
1405+
.impl(JsonFunctions.class, "fromJson")
1406+
.register();
1407+
1408+
context.function()
1409+
.name("getState")
1410+
.param("sessionId")
1411+
.impl(MyModule.class, "getState")
1412+
.register();
1413+
}
1414+
1415+
public static Object getStateData;
1416+
public static Map getState(long sessionId) { ... }
1417+
1418+
public void init(JactlEnv env) {
1419+
context = JactlContext.create()
1420+
.environment(env)
1421+
.hasOwnFunctions(true)
1422+
.build();
1423+
1424+
registerFunctions(context);
1425+
}
1426+
1427+
...
1428+
}
1429+
```
1430+
1431+
The way in which the function/method is registered is identical, except that we use the `JactlContext` object rather
1432+
than the `Jactl` class (as shown in the example).
1433+
1434+
Note that the `JactlContext` will also have access to all functions/methods that have already been registered using
1435+
`Jactl.function()` or `Jactl.method()` at the point at which the `JactlContext` is created.
1436+
If other functions/methods are later registered using `Jactl.function()` or `Jactl.method()` after the `JactlContext`
1437+
was created, these additional functions/methods will not be available to scripts compiled with that `JactlContext`.
1438+
1439+
### Deregistering Functions
1440+
1441+
It is possible to deregister a function/method so that it is no longer available to any new scripts that are compiled.
1442+
This might be useful in unit tests, for example.
1443+
1444+
To deregister a global function just pass the function name to `Jactl.deregister()`:
1445+
```java
1446+
Jactl.deregister("myFunction");
1447+
```
1448+
1449+
To deregister a function from a `JactlContext`:
1450+
```java
1451+
jactlContext.deregister("myFunction");
1452+
```
1453+
1454+
To deregister a method:
1455+
```java
1456+
Jactl.deregister(JactlType.STRING, "lines");
1457+
jactlContext.deregister(JactlType.LIST, "myListMethod");
1458+
```
1459+
13651460
## Example Application
13661461

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

13701465
See [Example Application](https://github.com/jaccomoc/jactl-vertx#example-application) for more details.

src/main/java/io/jactl/Jactl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,15 @@ public static JactlFunction method(JactlType type) {
240240
* @param name the name of the method
241241
*/
242242
public static void deregister(JactlType type, String name) {
243-
BuiltinFunctions.deregisterFunction(type, name);
243+
Functions.INSTANCE.deregisterFunction(type, name);
244244
}
245245

246246
/**
247247
* Deregister a global function
248248
* @param name the name of the global function
249249
*/
250250
public static void deregister(String name) {
251-
BuiltinFunctions.deregisterFunction(name);
251+
Functions.INSTANCE.deregisterFunction(name);
252252
}
253253

254254
////////////////////////////////////////////

src/main/java/io/jactl/JactlContext.java

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package io.jactl;
1919

20+
import io.jactl.compiler.Compiler;
2021
import io.jactl.runtime.*;
2122

2223
import java.io.File;
@@ -26,6 +27,7 @@
2627
import java.util.concurrent.atomic.AtomicInteger;
2728
import java.util.concurrent.atomic.AtomicLong;
2829
import java.util.function.Consumer;
30+
import java.util.function.Function;
2931

3032
public class JactlContext {
3133

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

44+
private final Map<String, Function<Map<String,Object>,Object>> evalScriptCache = Collections.synchronizedMap(
45+
new LinkedHashMap(16, 0.75f, true) {
46+
@Override
47+
protected boolean removeEldestEntry(Map.Entry eldest) {
48+
return size() > scriptCacheSize;
49+
}
50+
});
51+
52+
public static int scriptCacheSize = 100; // Total number of compiled scripts we keep for use by eval() function
53+
4254
// Testing
4355
boolean checkpoint = false;
4456
boolean restore = false;
@@ -70,6 +82,8 @@ public class JactlContext {
7082
private boolean isIdePlugin = false;
7183
private File buildDir;
7284

85+
private Functions functions;
86+
7387
DynamicClassLoader classLoader = new DynamicClassLoader();
7488

7589
///////////////////////////////
@@ -80,18 +94,18 @@ public static JactlContextBuilder create() {
8094

8195
private JactlContext() {}
8296

83-
/**
84-
* Get the current context by finding our thread current class loader.
85-
* @return the current context
86-
* @throws IllegalStateException if current class loader is not of correct type
87-
*/
88-
public static JactlContext getContext() {
89-
ClassLoader loader = Thread.currentThread().getContextClassLoader();
90-
if (loader instanceof DynamicClassLoader) {
91-
return ((DynamicClassLoader)loader).getJactlContext();
92-
}
93-
throw new IllegalStateException("Expected class loader of type " + DynamicClassLoader.class.getName() + " but found " + loader.getClass().getName());
94-
}
97+
// /**
98+
// * Get the current context by finding our thread current class loader.
99+
// * @return the current context
100+
// * @throws IllegalStateException if current class loader is not of correct type
101+
// */
102+
// public static JactlContext getContext() {
103+
// ClassLoader loader = Thread.currentThread().getContextClassLoader();
104+
// if (loader instanceof DynamicClassLoader) {
105+
// return ((DynamicClassLoader)loader).getJactlContext();
106+
// }
107+
// throw new IllegalStateException("Expected class loader of type " + DynamicClassLoader.class.getName() + " but found " + loader.getClass().getName());
108+
// }
95109

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

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

208223
//////////////////////////////////
209224

225+
public Functions getFunctions() {
226+
return functions == null ? Functions.INSTANCE : functions;
227+
}
228+
229+
public JactlFunction function() {
230+
if (functions == null) {
231+
throw new IllegalStateException("JactlContext was not built to have separate functions (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
232+
}
233+
return new JactlFunction(this);
234+
}
235+
236+
public JactlFunction method(JactlType methodClass) {
237+
if (functions == null) {
238+
throw new IllegalStateException("JactlContext was not built to have separate methods (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
239+
}
240+
return new JactlFunction(this, methodClass);
241+
}
242+
243+
public void deregister(String name) {
244+
if (functions == null) {
245+
throw new IllegalStateException("JactlContext was not built to have separate functions (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
246+
}
247+
getFunctions().deregisterFunction(name);
248+
}
249+
250+
public void deregister(JactlType type, String name) {
251+
if (functions == null) {
252+
throw new IllegalStateException("JactlContext was not built to have separate methods (hasOwnFunctions() needs to be invoked during build phase of JactlContext)");
253+
}
254+
getFunctions().deregisterFunction(type, name);
255+
}
256+
257+
public void clearScriptCache() {
258+
evalScriptCache.clear();
259+
}
260+
261+
public Function<Map<String, Object>, Object> getEvalScript(String code, Map bindings) {
262+
Function<Map<String, Object>, Object> script = evalScriptCache.get(code);
263+
if (script == null) {
264+
// For eval we want to be able to cache the scripts but the problem is that if the bindings
265+
// are typed (e.g. x is an Integer) but then when script is rerun a global has had its type
266+
// changed (e.g. x is now a Long) the script will fail because the types don't match. So
267+
// we erase all types and make everything ANY. This is obviously less efficient but for eval()
268+
// efficiency should not be a big issue.
269+
HashMap erasedBindings = new HashMap();
270+
bindings.keySet().forEach(k -> erasedBindings.put(k, null));
271+
script = Compiler.compileScriptInternal(code, this, Utils.DEFAULT_JACTL_PKG, erasedBindings);
272+
evalScriptCache.put(code, script);
273+
}
274+
return script;
275+
}
276+
210277
public boolean printLoop() { return printLoop; }
211278
public boolean nonPrintLoop() { return nonPrintLoop; }
212279

src/main/java/io/jactl/JactlScript.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private static void cleanUp(JactlScriptObject instance, JactlContext context) {
129129
* @param completion code to be run once script finishes
130130
*/
131131
public void run(Map<String,Object> globals, BufferedReader input, PrintStream output, Consumer<Object> completion) {
132-
RuntimeState.setState(globals, input, output);
132+
RuntimeState.setState(jactlContext, globals, input, output);
133133
script.accept(globals, completion);
134134
}
135135

src/main/java/io/jactl/Utils.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,14 @@ public static Expr.FunDecl createWrapperFunDecl(Token token, String name, boolea
980980
}
981981

982982
public static Method findStaticMethod(Class clss, String methodName) {
983-
return findMethod(clss, methodName, true);
983+
if (clss == null) {
984+
throw new IllegalArgumentException("Implementation class not specified");
985+
}
986+
Method method = findMethod(clss, methodName, true);
987+
if (method == null) {
988+
throw new IllegalArgumentException("Could not find method " + methodName + " in " + clss.getName());
989+
}
990+
return method;
984991
}
985992

986993
public static Method findMethod(Class clss, String methodName, boolean isStatic) {

src/main/java/io/jactl/compiler/MethodCompiler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,10 @@ void compileRegexMatch(Expr.RegexMatch expr, Runnable compileString) {
11851185
else {
11861186
// We need to find the method handle for the given method
11871187
FunctionDescriptor method = clss.getMethod(name);
1188+
if (method == null) {
1189+
// Look for builtin method
1190+
method = classCompiler.context.getFunctions().lookupMethod(clss.getInstanceType(), name);
1191+
}
11881192
check(method != null, "could not find method or field called " + name + " for " + expr.left.type);
11891193
// We want the handle to the wrapper method.
11901194
loadWrapperHandle(method, expr);
@@ -1787,7 +1791,7 @@ private void loadIndexField(Expr.Binary expr, JactlType parentType) {
17871791
if (isBuiltinFunction) {
17881792
// If we have the name of a built-in function then lookup its method handle
17891793
loadConst(name);
1790-
invokeMethod(BuiltinFunctions.class, "lookupMethodHandle", String.class);
1794+
invokeMethod(RuntimeUtils.class, RuntimeUtils.LOOKUP_METHOD_HANDLE, String.class);
17911795
}
17921796
else
17931797
if (name.charAt(0) == '$' && Utils.isDigits(name.substring(1))) {

0 commit comments

Comments
 (0)