Skip to content

Support array result include sequence action #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,36 @@ If needed you can also customize the method name of your Java action. This
can be done by specifying the Java fully-qualified method name of your action,
e.q., `--main com.example.MyMain#methodName`

Not only support return JsonObject but also support return JsonArray, the main function would be:

```java
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

public class HelloArray {
public static JsonArray main(JsonObject args) {
JsonArray jsonArray = new JsonArray();
jsonArray.add("a");
jsonArray.add("b");
return jsonArray;
}
}
```

And support array result for sequence action as well, the first action's array result can be used as next action's input parameter.

So the function would be:

```java
import com.google.gson.JsonArray;

public class Sort {
public static JsonArray main(JsonArray args) {
return args;
}
}
```

### Create the Java Action
To use as a docker action:
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
import java.util.Collections;
import java.util.Map;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

public class JarLoader extends URLClassLoader {
private final Class<?> mainClass;
private final Method mainMethod;
public final Class<?> mainClass;
public final String entrypointMethodName;

public static Path saveBase64EncodedFile(InputStream encoded) throws Exception {
Base64.Decoder decoder = Base64.getDecoder();
Expand All @@ -58,26 +60,24 @@ public JarLoader(Path jarPath, String entrypoint)

final String[] splittedEntrypoint = entrypoint.split("#");
final String entrypointClassName = splittedEntrypoint[0];
final String entrypointMethodName = splittedEntrypoint.length > 1 ? splittedEntrypoint[1] : "main";

this.mainClass = loadClass(entrypointClassName);

Method m = mainClass.getMethod(entrypointMethodName, new Class[] { JsonObject.class });
m.setAccessible(true);
int modifiers = m.getModifiers();
if (m.getReturnType() != JsonObject.class || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
throw new NoSuchMethodException("main");
this.entrypointMethodName = splittedEntrypoint.length > 1 ? splittedEntrypoint[1] : "main";
Method[] methods = mainClass.getDeclaredMethods();
Boolean existMain = false;
for(Method method: methods) {
if (method.getName().equals(this.entrypointMethodName)) {
existMain = true;
break;
}
}
if (!existMain) {
throw new NoSuchMethodException(this.entrypointMethodName);
}
this.mainMethod = m;
}

public JsonObject invokeMain(JsonObject arg, Map<String, String> env) throws Exception {
augmentEnv(env);
return (JsonObject) mainMethod.invoke(null, arg);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private static void augmentEnv(Map<String, String> newEnv) {
public static void augmentEnv(Map<String, String> newEnv) {
try {
for (Class cl : Collections.class.getDeclaredClasses()) {
if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
Expand Down Expand Up @@ -139,7 +142,17 @@ public void handle(HttpExchange t) throws IOException {
InputStream is = t.getRequestBody();
JsonParser parser = new JsonParser();
JsonObject body = parser.parse(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))).getAsJsonObject();
JsonObject inputObject = body.getAsJsonObject("value");
JsonParser json = new JsonParser();
JsonObject payloadForJsonObject = json.parse("{}").getAsJsonObject();
JsonArray payloadForJsonArray = json.parse("[]").getAsJsonArray();
Boolean isJsonObjectParam = true;
JsonElement inputJsonElement = body.get("value");
if (inputJsonElement.isJsonObject()) {
payloadForJsonObject = inputJsonElement.getAsJsonObject();
} else {
payloadForJsonArray = inputJsonElement.getAsJsonArray();
isJsonObjectParam = false;
}

HashMap<String, String> env = new HashMap<String, String>();
Set<Map.Entry<String, JsonElement>> entrySet = body.entrySet();
Expand All @@ -153,8 +166,28 @@ public void handle(HttpExchange t) throws IOException {
Thread.currentThread().setContextClassLoader(loader);
System.setSecurityManager(new WhiskSecurityManager());

// User code starts running here.
JsonObject output = loader.invokeMain(inputObject, env);
Method mainMethod = null;
String mainMethodName = loader.entrypointMethodName;
if (isJsonObjectParam) {
mainMethod = loader.mainClass.getMethod(mainMethodName, new Class[] { JsonObject.class });
} else {
mainMethod = loader.mainClass.getMethod(mainMethodName, new Class[] { JsonArray.class });
}
mainMethod.setAccessible(true);
int modifiers = mainMethod.getModifiers();
if ((mainMethod.getReturnType() != JsonObject.class && mainMethod.getReturnType() != JsonArray.class) || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
throw new NoSuchMethodException(mainMethodName);
}

// User code starts running here. the return object supports JsonObject and JsonArray both.
Object output;
if (isJsonObjectParam) {
loader.augmentEnv(env);
output = mainMethod.invoke(null, payloadForJsonObject);
} else {
loader.augmentEnv(env);
output = mainMethod.invoke(null, payloadForJsonArray);
}
// User code finished running here.

if (output == null) {
Expand Down
8 changes: 4 additions & 4 deletions core/java8actionloop/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#

# build go proxy from source
FROM golang:1.16 AS builder_source
FROM golang:1.18 AS builder_source
ARG GO_PROXY_GITHUB_USER=apache
ARG GO_PROXY_GITHUB_BRANCH=master
RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} \
Expand All @@ -25,13 +25,13 @@ RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} \
mv proxy /bin/proxy

# or build it from a release
FROM golang:1.16 AS builder_release
ARG GO_PROXY_RELEASE_VERSION=1.16@1.19.0
FROM golang:1.18 AS builder_release
ARG GO_PROXY_RELEASE_VERSION=1.18@1.20.0
RUN curl -sL \
https://github.com/apache/openwhisk-runtime-go/archive/{$GO_PROXY_RELEASE_VERSION}.tar.gz\
| tar xzf -\
&& cd openwhisk-runtime-go-*/main\
&& GO111MODULE=on go build -o /bin/proxy
&& GO111MODULE=on CGO_ENABLED=0 go build -o /bin/proxy

# Use AdoptOpenJDK's JDK8, OpenJ9, ubuntu
FROM ibm-semeru-runtimes:open-8u332-b09-jdk-focal
Expand Down
52 changes: 41 additions & 11 deletions core/java8actionloop/lib/src/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,22 @@ private static void initMain(String[] args) throws Exception {
}

mainClass = Class.forName(mainClassName);
Method m = mainClass.getMethod(mainMethodName, new Class[] { JsonObject.class });
m.setAccessible(true);
int modifiers = m.getModifiers();
if (m.getReturnType() != JsonObject.class || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
Method[] methods = mainClass.getDeclaredMethods();
Boolean existMain = false;
for(Method method: methods) {
if (method.getName().equals(mainMethodName)) {
existMain = true;
break;
}
}
if (!existMain) {
throw new NoSuchMethodException(mainMethodName);
}
mainMethod = m;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because need to support array result, cannot instantiate mainMethod in /init step, should move this relative logic to /run logic due to input param/out result can be JsonObject or JsonArray

}

private static JsonObject invokeMain(JsonObject arg, Map<String, String> env) throws Exception {
private static Object invokeMain(JsonElement arg, Map<String, String> env) throws Exception {
augmentEnv(env);
return (JsonObject) mainMethod.invoke(null, arg);
return mainMethod.invoke(null, arg);
}

private static SecurityManager defaultSecurityManager = null;
Expand Down Expand Up @@ -119,30 +123,56 @@ public static void main(String[] args) throws Exception {
new OutputStreamWriter(
new FileOutputStream("/dev/fd/3"), "UTF-8"));
JsonParser json = new JsonParser();
JsonObject empty = json.parse("{}").getAsJsonObject();
JsonObject emptyForJsonObject = json.parse("{}").getAsJsonObject();
JsonArray emptyForJsonArray = json.parse("[]").getAsJsonArray();
Boolean isJsonObjectParam = true;
String input = "";
while (true) {
try {
input = in.readLine();
if (input == null)
break;
JsonElement element = json.parse(input);
JsonObject payload = empty.deepCopy();
JsonObject payloadForJsonObject = emptyForJsonObject.deepCopy();
JsonArray payloadForJsonArray = emptyForJsonArray.deepCopy();
HashMap<String, String> env = new HashMap<String, String>();
if (element.isJsonObject()) {
// collect payload and environment
for (Map.Entry<String, JsonElement> entry : element.getAsJsonObject().entrySet()) {
if (entry.getKey().equals("value")) {
if (entry.getValue().isJsonObject())
payload = entry.getValue().getAsJsonObject();
payloadForJsonObject = entry.getValue().getAsJsonObject();
else {
payloadForJsonArray = entry.getValue().getAsJsonArray();
isJsonObjectParam = false;
}
} else {
env.put(String.format("__OW_%s", entry.getKey().toUpperCase()),
entry.getValue().getAsString());
}
}
augmentEnv(env);
}
JsonElement response = invokeMain(payload, env);

Method m = null;
if (isJsonObjectParam) {
m = mainClass.getMethod(mainMethodName, new Class[] { JsonObject.class });
} else {
m = mainClass.getMethod(mainMethodName, new Class[] { JsonArray.class });
}
m.setAccessible(true);
int modifiers = m.getModifiers();
if ((m.getReturnType() != JsonObject.class && m.getReturnType() != JsonArray.class) || !Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {
throw new NoSuchMethodException(mainMethodName);
}
mainMethod = m;

Object response;
if (isJsonObjectParam) {
response = invokeMain(payloadForJsonObject, env);
} else {
response = invokeMain(payloadForJsonArray, env);
}
out.println(response.toString());
} catch(NullPointerException npe) {
System.out.println("the action returned null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,7 @@ class JavaActionContainerTests extends BasicActionRunnerTests with WskActorSyste

val expected = m match {
case c if c == "x" || c == "!" => s"$errPrefix java.lang.ClassNotFoundException: example.HelloWhisk$c"
case "#bogus" =>
s"$errPrefix java.lang.NoSuchMethodException: example.HelloWhisk.bogus(com.google.gson.JsonObject)"
case _ => s"$errPrefix java.lang.NoSuchMethodException: example.HelloWhisk.main(com.google.gson.JsonObject)"
case _ => s"$errPrefix java.lang.NoSuchMethodException"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to https://github.com/apache/openwhisk-runtime-java/pull/140/files#r939487201, need to change test case here for a small change as above.

}

val error = out.get.fields.get("error").get.toString()
Expand Down Expand Up @@ -301,6 +299,56 @@ class JavaActionContainerTests extends BasicActionRunnerTests with WskActorSyste
})
}

it should "support return array result" in {
val (out, err) = withActionContainer() { c =>
val jar = JarBuilder.mkBase64Jar(
Seq("", "HelloArrayWhisk.java") ->
"""
| import com.google.gson.JsonArray;
| import com.google.gson.JsonObject;
|
| public class HelloArrayWhisk {
| public static JsonArray main(JsonObject args) throws Exception {
| JsonArray jsonArray = new JsonArray();
| jsonArray.add("a");
| jsonArray.add("b");
| return jsonArray;
| }
| }
""".stripMargin.trim)

val (initCode, _) = c.init(initPayload(jar, "HelloArrayWhisk"))
initCode should be(200)

val (runCode, runRes) = c.runForJsArray(runPayload(JsObject()))
runCode should be(200)
runRes shouldBe Some(JsArray(JsString("a"), JsString("b")))
}
}

it should "support array as input param" in {
val (out, err) = withActionContainer() { c =>
val jar = JarBuilder.mkBase64Jar(
Seq("", "HelloArrayWhisk.java") ->
"""
| import com.google.gson.JsonArray;
|
| public class HelloArrayWhisk {
| public static JsonArray main(JsonArray args) throws Exception {
| return args;
| }
| }
""".stripMargin.trim)

val (initCode, _) = c.init(initPayload(jar, "HelloArrayWhisk"))
initCode should be(200)

val (runCode, runRes) = c.runForJsArray(runPayload(JsArray(JsString("a"), JsString("b"))))
runCode should be(200)
runRes shouldBe Some(JsArray(JsString("a"), JsString("b")))
}
}

it should "survive System.exit" in {
val (out, err) = withActionContainer() { c =>
val jar = JarBuilder.mkBase64Jar(
Expand Down