Apache OpenWhisk Java Actions: Complete Implementation Guide

Introduction

Apache OpenWhisk is a serverless, open-source cloud platform that executes functions in response to events. Java actions in OpenWhisk allow you to run Java code in a serverless environment, providing the benefits of Java's ecosystem with the scalability and cost-efficiency of serverless computing.


Architecture Overview

[HTTP Trigger] → [OpenWhisk Controller] → [Java Action] → [Response]
↓                ↓                     ↓             ↓
Webhook         Invoker            JVM Container     JSON Response
API Gateway     Load Balancer      Class Loader      Error Handling
Schedule        Authentication     Dependency Mgmt   Logging

Step 1: Project Setup and Dependencies

Maven Configuration

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>openwhisk-java-actions</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project-build.sourceEncoding>
<openwhisk.version>1.0.0</openwhisk.version>
<jackson.version>2.15.0</jackson.version>
<gson.version>2.10.1</gson.version>
<okhttp.version>4.11.0</okhttp.version>
<junit.version>5.10.0</junit.version>
</properties>
<dependencies>
<!-- OpenWhisk Java Client -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.openwhisk.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.example.openwhisk.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Step 2: OpenWhisk Action Interface and Base Classes

Core Action Interface

Action.java

package com.example.openwhisk.core;
import java.util.Map;
/**
* Base interface for all OpenWhisk actions
*/
@FunctionalInterface
public interface Action {
/**
* Main method that processes the input and returns a result
* @param input The input parameters as a Map
* @return The result as a Map that will be converted to JSON
*/
Map<String, Object> execute(Map<String, Object> input);
/**
* Default method for input validation
* @param input The input parameters
* @throws ActionException if validation fails
*/
default void validateInput(Map<String, Object> input) throws ActionException {
// Default implementation does nothing
// Override in subclasses for specific validation
}
/**
* Default method for logging execution
* @param input The input parameters
* @param result The execution result
* @param duration Execution duration in milliseconds
*/
default void logExecution(Map<String, Object> input, Map<String, Object> result, long duration) {
System.out.printf("Action executed in %d ms. Input: %s, Result: %s%n", 
duration, input, result);
}
}

Action Exception

ActionException.java

package com.example.openwhisk.core;
public class ActionException extends RuntimeException {
private final String errorCode;
private final int statusCode;
public ActionException(String message) {
super(message);
this.errorCode = "ACTION_ERROR";
this.statusCode = 500;
}
public ActionException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
this.statusCode = 500;
}
public ActionException(String message, String errorCode, int statusCode) {
super(message);
this.errorCode = errorCode;
this.statusCode = statusCode;
}
public ActionException(String message, Throwable cause) {
super(message, cause);
this.errorCode = "ACTION_ERROR";
this.statusCode = 500;
}
// Getters
public String getErrorCode() { return errorCode; }
public int getStatusCode() { return statusCode; }
public Map<String, Object> toResponse() {
return Map.of(
"error", true,
"code", errorCode,
"message", getMessage(),
"statusCode", statusCode
);
}
}

Abstract Base Action

AbstractAction.java

package com.example.openwhisk.core;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Abstract base class for OpenWhisk actions with common functionality
*/
public abstract class AbstractAction implements Action {
private final String actionName;
private final String version;
protected AbstractAction(String actionName) {
this(actionName, "1.0.0");
}
protected AbstractAction(String actionName, String version) {
this.actionName = actionName;
this.version = version;
}
@Override
public final Map<String, Object> execute(Map<String, Object> input) {
long startTime = System.currentTimeMillis();
Map<String, Object> result;
try {
// Validate input
validateInput(input);
// Execute the action
result = doExecute(input);
// Ensure result is not null
if (result == null) {
result = Map.of("success", true);
}
// Add metadata
result = enhanceResult(result, input);
} catch (ActionException e) {
result = e.toResponse();
} catch (Exception e) {
result = new ActionException("Internal server error", "INTERNAL_ERROR", 500)
.toResponse();
}
// Log execution
long duration = System.currentTimeMillis() - startTime;
logExecution(input, result, duration);
return result;
}
/**
* Template method for actual execution logic
*/
protected abstract Map<String, Object> doExecute(Map<String, Object> input) throws ActionException;
/**
* Enhance result with metadata
*/
protected Map<String, Object> enhanceResult(Map<String, Object> result, Map<String, Object> input) {
// Don't enhance error responses
if (result.containsKey("error") && (Boolean) result.get("error")) {
return result;
}
// Create enhanced result
return Map.of(
"data", result,
"metadata", Map.of(
"action", actionName,
"version", version,
"timestamp", System.currentTimeMillis(),
"success", true
)
);
}
/**
* Get string parameter from input with default value
*/
protected String getStringParam(Map<String, Object> input, String key, String defaultValue) {
Object value = input.get(key);
return value != null ? value.toString() : defaultValue;
}
/**
* Get required string parameter from input
*/
protected String getRequiredStringParam(Map<String, Object> input, String key) throws ActionException {
Object value = input.get(key);
if (value == null) {
throw new ActionException("Missing required parameter: " + key, "MISSING_PARAMETER", 400);
}
return value.toString();
}
/**
* Get integer parameter from input with default value
*/
protected int getIntParam(Map<String, Object> input, String key, int defaultValue) {
try {
Object value = input.get(key);
return value != null ? Integer.parseInt(value.toString()) : defaultValue;
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Get boolean parameter from input with default value
*/
protected boolean getBooleanParam(Map<String, Object> input, String key, boolean defaultValue) {
Object value = input.get(key);
if (value == null) {
return defaultValue;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return Boolean.parseBoolean(value.toString());
}
// Getters
public String getActionName() { return actionName; }
public String getVersion() { return version; }
}

Step 3: Sample Action Implementations

1. Hello World Action

HelloWorldAction.java

package com.example.openwhisk.actions;
import com.example.openwhisk.core.AbstractAction;
import com.example.openwhisk.core.ActionException;
import java.util.Map;
/**
* Simple Hello World action that demonstrates basic OpenWhisk functionality
*/
public class HelloWorldAction extends AbstractAction {
public HelloWorldAction() {
super("hello-world");
}
@Override
protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException {
String name = getStringParam(input, "name", "World");
String greeting = getStringParam(input, "greeting", "Hello");
String message = String.format("%s, %s!", greeting, name);
return Map.of(
"message", message,
"input", input,
"timestamp", System.currentTimeMillis()
);
}
@Override
public void validateInput(Map<String, Object> input) throws ActionException {
// Optional: Add input validation
String name = getStringParam(input, "name", "");
if (name.length() > 100) {
throw new ActionException("Name too long", "VALIDATION_ERROR", 400);
}
}
}

2. JSON Processing Action

JsonProcessorAction.java

package com.example.openwhisk.actions;
import com.example.openwhisk.core.AbstractAction;
import com.example.openwhisk.core.ActionException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
/**
* Action that processes JSON data with validation and transformation
*/
public class JsonProcessorAction extends AbstractAction {
private final ObjectMapper objectMapper;
public JsonProcessorAction() {
super("json-processor");
this.objectMapper = new ObjectMapper();
}
@Override
protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException {
try {
// Get JSON data from input
String jsonData = getRequiredStringParam(input, "data");
String operation = getStringParam(input, "operation", "validate");
// Parse JSON
Object parsedData = objectMapper.readValue(jsonData, Object.class);
// Perform operation
Object result;
switch (operation) {
case "validate":
result = validateJson(parsedData);
break;
case "transform":
result = transformJson(parsedData, input);
break;
case "extract":
result = extractFields(parsedData, input);
break;
default:
throw new ActionException("Unknown operation: " + operation, "INVALID_OPERATION", 400);
}
return Map.of(
"operation", operation,
"input", parsedData,
"result", result,
"success", true
);
} catch (JsonProcessingException e) {
throw new ActionException("Invalid JSON data: " + e.getMessage(), "INVALID_JSON", 400);
}
}
private Map<String, Object> validateJson(Object data) {
boolean isValid = data != null;
return Map.of(
"valid", isValid,
"size", data instanceof Map ? ((Map<?, ?>) data).size() : 0,
"type", data != null ? data.getClass().getSimpleName() : "null"
);
}
@SuppressWarnings("unchecked")
private Object transformJson(Object data, Map<String, Object> input) {
if (!(data instanceof Map)) {
return data;
}
Map<String, Object> dataMap = (Map<String, Object>) data;
String transformType = getStringParam(input, "transformType", "uppercase");
switch (transformType) {
case "uppercase":
return transformToUppercase(dataMap);
case "flatten":
return flattenJson(dataMap);
case "filter":
return filterFields(dataMap, input);
default:
return dataMap;
}
}
private Map<String, Object> transformToUppercase(Map<String, Object> data) {
return data.entrySet().stream()
.collect(java.util.stream.Collectors.toMap(
entry -> entry.getKey().toUpperCase(),
entry -> {
Object value = entry.getValue();
if (value instanceof String) {
return ((String) value).toUpperCase();
}
return value;
}
));
}
private Map<String, Object> flattenJson(Map<String, Object> data) {
Map<String, Object> flattened = new java.util.HashMap<>();
flattenJson("", data, flattened);
return flattened;
}
@SuppressWarnings("unchecked")
private void flattenJson(String prefix, Map<String, Object> data, Map<String, Object> result) {
data.forEach((key, value) -> {
String newKey = prefix.isEmpty() ? key : prefix + "." + key;
if (value instanceof Map) {
flattenJson(newKey, (Map<String, Object>) value, result);
} else {
result.put(newKey, value);
}
});
}
@SuppressWarnings("unchecked")
private Object extractFields(Object data, Map<String, Object> input) {
if (!(data instanceof Map)) {
return data;
}
Map<String, Object> dataMap = (Map<String, Object>) data;
String fields = getStringParam(input, "fields", "");
if (fields.isEmpty()) {
return dataMap;
}
return java.util.Arrays.stream(fields.split(","))
.map(String::trim)
.filter(dataMap::containsKey)
.collect(java.util.stream.Collectors.toMap(
field -> field,
dataMap::get
));
}
}

3. HTTP Client Action

HttpClientAction.java

package com.example.openwhisk.actions;
import com.example.openwhisk.core.AbstractAction;
import com.example.openwhisk.core.ActionException;
import okhttp3.*;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Action that makes HTTP requests to external APIs
*/
public class HttpClientAction extends AbstractAction {
private final OkHttpClient httpClient;
public HttpClientAction() {
super("http-client");
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
}
@Override
protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException {
String url = getRequiredStringParam(input, "url");
String method = getStringParam(input, "method", "GET");
String body = getStringParam(input, "body", "");
Map<String, String> headers = getHeaders(input);
try {
Request request = buildRequest(url, method, body, headers);
Response response = httpClient.newCall(request).execute();
return processResponse(response);
} catch (IOException e) {
throw new ActionException("HTTP request failed: " + e.getMessage(), "HTTP_ERROR", 502);
}
}
@SuppressWarnings("unchecked")
private Map<String, String> getHeaders(Map<String, Object> input) {
Object headersObj = input.get("headers");
if (headersObj instanceof Map) {
return (Map<String, String>) headersObj;
}
return Map.of();
}
private Request buildRequest(String url, String method, String body, Map<String, String> headers) {
Request.Builder requestBuilder = new Request.Builder().url(url);
// Add headers
headers.forEach(requestBuilder::addHeader);
// Set method and body
if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) {
MediaType mediaType = MediaType.parse("application/json");
RequestBody requestBody = RequestBody.create(body, mediaType);
requestBuilder.method(method, requestBody);
} else {
requestBuilder.method(method, null);
}
return requestBuilder.build();
}
private Map<String, Object> processResponse(Response response) throws IOException {
String responseBody = response.body() != null ? response.body().string() : "";
return Map.of(
"statusCode", response.code(),
"statusMessage", response.message(),
"headers", response.headers().toMultimap(),
"body", responseBody,
"success", response.isSuccessful(),
"timestamp", System.currentTimeMillis()
);
}
@Override
public void validateInput(Map<String, Object> input) throws ActionException {
super.validateInput(input);
String url = getRequiredStringParam(input, "url");
if (!url.startsWith("http://") && !url.startsWith("https://")) {
throw new ActionException("Invalid URL protocol", "INVALID_URL", 400);
}
String method = getStringParam(input, "method", "GET").toUpperCase();
if (!java.util.Set.of("GET", "POST", "PUT", "DELETE", "PATCH").contains(method)) {
throw new ActionException("Unsupported HTTP method: " + method, "INVALID_METHOD", 400);
}
}
}

4. Database Action (with connection pooling)

DatabaseAction.java

package com.example.openwhisk.actions;
import com.example.openwhisk.core.AbstractAction;
import com.example.openwhisk.core.ActionException;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.sql.*;
import java.util.*;
/**
* Action that performs database operations
* Note: In production, use connection pooling and proper secret management
*/
public class DatabaseAction extends AbstractAction {
private DataSource dataSource;
public DatabaseAction() {
super("database");
initializeDataSource();
}
private void initializeDataSource() {
// In production, these would come from environment variables or secrets
String jdbcUrl = System.getenv("DB_URL");
String username = System.getenv("DB_USERNAME");
String password = System.getenv("DB_PASSWORD");
if (jdbcUrl == null) {
// For demo purposes, use in-memory H2
jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1";
username = "sa";
password = "";
}
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(5);
config.setMinimumIdle(1);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
this.dataSource = new HikariDataSource(config);
}
@Override
protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException {
String operation = getRequiredStringParam(input, "operation");
String query = getStringParam(input, "query", "");
Map<String, Object> parameters = getParameters(input);
try (Connection connection = dataSource.getConnection()) {
switch (operation.toLowerCase()) {
case "query":
return executeQuery(connection, query, parameters);
case "update":
return executeUpdate(connection, query, parameters);
case "execute":
return executeStatement(connection, query);
default:
throw new ActionException("Unknown operation: " + operation, "INVALID_OPERATION", 400);
}
} catch (SQLException e) {
throw new ActionException("Database error: " + e.getMessage(), "DATABASE_ERROR", 500);
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> getParameters(Map<String, Object> input) {
Object paramsObj = input.get("parameters");
if (paramsObj instanceof Map) {
return (Map<String, Object>) paramsObj;
}
return Map.of();
}
private Map<String, Object> executeQuery(Connection connection, String query, 
Map<String, Object> parameters) throws SQLException {
try (PreparedStatement stmt = connection.prepareStatement(query)) {
setParameters(stmt, parameters);
try (ResultSet rs = stmt.executeQuery()) {
List<Map<String, Object>> results = new ArrayList<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()) {
Map<String, Object> row = new HashMap<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
Object value = rs.getObject(i);
row.put(columnName, value);
}
results.add(row);
}
return Map.of(
"operation", "query",
"rowCount", results.size(),
"results", results,
"success", true
);
}
}
}
private Map<String, Object> executeUpdate(Connection connection, String query,
Map<String, Object> parameters) throws SQLException {
try (PreparedStatement stmt = connection.prepareStatement(query)) {
setParameters(stmt, parameters);
int affectedRows = stmt.executeUpdate();
return Map.of(
"operation", "update",
"affectedRows", affectedRows,
"success", true
);
}
}
private Map<String, Object> executeStatement(Connection connection, String query) throws SQLException {
try (Statement stmt = connection.createStatement()) {
boolean hasResultSet = stmt.execute(query);
if (hasResultSet) {
try (ResultSet rs = stmt.getResultSet()) {
List<Map<String, Object>> results = new ArrayList<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()) {
Map<String, Object> row = new HashMap<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
Object value = rs.getObject(i);
row.put(columnName, value);
}
results.add(row);
}
return Map.of(
"operation", "execute",
"rowCount", results.size(),
"results", results,
"success", true
);
}
} else {
int updateCount = stmt.getUpdateCount();
return Map.of(
"operation", "execute",
"updateCount", updateCount,
"success", true
);
}
}
}
private void setParameters(PreparedStatement stmt, Map<String, Object> parameters) throws SQLException {
int index = 1;
for (Map.Entry<String, Object> entry : parameters.entrySet()) {
// Handle parameter names like :param or $1
if (entry.getKey().startsWith(":") || entry.getKey().startsWith("$")) {
stmt.setObject(index++, entry.getValue());
}
}
}
@Override
public void validateInput(Map<String, Object> input) throws ActionException {
super.validateInput(input);
String operation = getRequiredStringParam(input, "operation");
if (!java.util.Set.of("query", "update", "execute").contains(operation.toLowerCase())) {
throw new ActionException("Invalid operation: " + operation, "INVALID_OPERATION", 400);
}
if (operation.equalsIgnoreCase("query") || operation.equalsIgnoreCase("update")) {
String query = getStringParam(input, "query", "");
if (query.trim().isEmpty()) {
throw new ActionException("Query is required for " + operation + " operation", 
"MISSING_QUERY", 400);
}
}
}
}

Step 4: Action Factory and Registry

Action Factory

ActionFactory.java

package com.example.openwhisk.factory;
import com.example.openwhisk.core.Action;
import com.example.openwhisk.core.ActionException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Factory for creating and managing OpenWhisk actions
*/
public class ActionFactory {
private static final Map<String, Action> actionRegistry = new ConcurrentHashMap<>();
private static final Map<String, Class<? extends Action>> actionClasses = new ConcurrentHashMap<>();
static {
// Register action classes
registerActionClass("hello", com.example.openwhisk.actions.HelloWorldAction.class);
registerActionClass("json", com.example.openwhisk.actions.JsonProcessorAction.class);
registerActionClass("http", com.example.openwhisk.actions.HttpClientAction.class);
registerActionClass("db", com.example.openwhisk.actions.DatabaseAction.class);
}
/**
* Register an action class
*/
public static void registerActionClass(String actionType, Class<? extends Action> actionClass) {
actionClasses.put(actionType, actionClass);
}
/**
* Create or get an action instance
*/
public static Action getAction(String actionType) throws ActionException {
return actionRegistry.computeIfAbsent(actionType, type -> {
try {
Class<? extends Action> actionClass = actionClasses.get(type);
if (actionClass == null) {
throw new ActionException("Unknown action type: " + type, "UNKNOWN_ACTION", 400);
}
return actionClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new ActionException("Failed to create action: " + e.getMessage(), 
"ACTION_CREATION_ERROR", 500);
}
});
}
/**
* Execute an action by type
*/
public static Map<String, Object> executeAction(String actionType, Map<String, Object> input) {
try {
Action action = getAction(actionType);
return action.execute(input);
} catch (ActionException e) {
return e.toResponse();
}
}
/**
* Get all registered action types
*/
public static java.util.Set<String> getRegisteredActionTypes() {
return java.util.Collections.unmodifiableSet(actionClasses.keySet());
}
/**
* Clear action cache (useful for testing)
*/
public static void clearCache() {
actionRegistry.clear();
}
}

Action Router

ActionRouter.java

package com.example.openwhisk.router;
import com.example.openwhisk.core.ActionException;
import com.example.openwhisk.factory.ActionFactory;
import java.util.Map;
/**
* Routes requests to appropriate actions based on input parameters
*/
public class ActionRouter {
/**
* Route and execute action based on input parameters
*/
public static Map<String, Object> routeAndExecute(Map<String, Object> input) {
try {
// Determine action type from input
String actionType = determineActionType(input);
// Execute the action
return ActionFactory.executeAction(actionType, input);
} catch (ActionException e) {
return e.toResponse();
} catch (Exception e) {
return new ActionException("Routing error: " + e.getMessage(), "ROUTING_ERROR", 500)
.toResponse();
}
}
/**
* Determine which action to execute based on input parameters
*/
private static String determineActionType(Map<String, Object> input) throws ActionException {
// Check for explicit action parameter
String explicitAction = (String) input.get("_action");
if (explicitAction != null && !explicitAction.trim().isEmpty()) {
return explicitAction.trim();
}
// Auto-detect based on input parameters
if (input.containsKey("url") && input.containsKey("method")) {
return "http";
} else if (input.containsKey("data") && input.get("data") instanceof String) {
return "json";
} else if (input.containsKey("operation") && "database".equals(input.get("operation"))) {
return "db";
} else if (input.containsKey("name") || input.containsKey("greeting")) {
return "hello";
}
throw new ActionException("Could not determine action type from input", 
"UNKNOWN_ACTION_TYPE", 400);
}
/**
* Validate input structure
*/
public static void validateInput(Map<String, Object> input) throws ActionException {
if (input == null || input.isEmpty()) {
throw new ActionException("Input cannot be empty", "EMPTY_INPUT", 400);
}
// Check for required fields based on auto-detected action type
String actionType = determineActionType(input);
switch (actionType) {
case "http":
if (!input.containsKey("url")) {
throw new ActionException("URL is required for HTTP actions", "MISSING_URL", 400);
}
break;
case "json":
if (!input.containsKey("data")) {
throw new ActionException("Data is required for JSON actions", "MISSING_DATA", 400);
}
break;
case "db":
if (!input.containsKey("operation")) {
throw new ActionException("Operation is required for database actions", 
"MISSING_OPERATION", 400);
}
break;
}
}
}

Step 5: Main Entry Point

OpenWhisk Main Class

Main.java

package com.example.openwhisk;
import com.example.openwhisk.core.ActionException;
import com.example.openwhisk.router.ActionRouter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;
/**
* Main entry point for OpenWhisk Java actions
* This class handles the JSON input/output expected by the OpenWhisk platform
*/
public class Main {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* Main method that OpenWhisk calls
* @param args Command line arguments (not typically used)
*/
public static void main(String[] args) {
try {
// Read input from stdin (OpenWhisk provides JSON input)
Map<String, Object> input = readInput();
// Route and execute the action
Map<String, Object> result = ActionRouter.routeAndExecute(input);
// Write result to stdout (OpenWhisk expects JSON output)
writeOutput(result);
} catch (Exception e) {
// Handle unexpected errors
Map<String, Object> errorResult = new ActionException(
"Unexpected error: " + e.getMessage(), "UNEXPECTED_ERROR", 500)
.toResponse();
writeOutput(errorResult);
System.exit(1);
}
}
/**
* Read JSON input from stdin
*/
private static Map<String, Object> readInput() throws IOException {
try {
// Read all input from stdin
String inputJson = new String(System.in.readAllBytes());
if (inputJson == null || inputJson.trim().isEmpty()) {
return Map.of();
}
// Parse JSON input
return objectMapper.readValue(inputJson, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
System.err.println("Error reading input: " + e.getMessage());
return Map.of();
}
}
/**
* Write JSON output to stdout
*/
private static void writeOutput(Map<String, Object> result) {
try {
String outputJson = objectMapper.writeValueAsString(result);
System.out.println(outputJson);
} catch (IOException e) {
System.err.println("Error writing output: " + e.getMessage());
// Fallback to simple output
System.out.println("{\"error\": true, \"message\": \"Output serialization failed\"}");
}
}
/**
* Alternative main method for testing outside OpenWhisk
*/
public static Map<String, Object> testMain(Map<String, Object> input) {
return ActionRouter.routeAndExecute(input);
}
}

Step 6: Deployment Configuration

Dockerfile for OpenWhisk Java Action

Dockerfile

FROM openwhisk/java8action:latest
# Switch to root to install additional packages
USER root
# Install any additional system dependencies
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
# Switch back to non-root user
USER 1001
# Copy the JAR file
COPY target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar /action/action.jar
# Set the main class
ENV OW_EXECUTION_ENV=java
ENV OW_COMPILER_FLAGS="--release 11"

OpenWhisk Manifest Files

deploy/action.yaml

packages:
java-demo:
actions:
hello-world:
function: target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar
runtime: java
main: com.example.openwhisk.Main
limits:
timeout: 30000
memory: 256
annotations:
provide-api-key: true
web-export: true
json-processor:
function: target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar
runtime: java
main: com.example.openwhisk.Main
limits:
timeout: 60000
memory: 512
annotations:
provide-api-key: true
web-export: true
http-client:
function: target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar
runtime: java
main: com.example.openwhisk.Main
limits:
timeout: 120000
memory: 512
annotations:
provide-api-key: true
web-export: false
sequences:
process-and-store:
actions: json-processor, http-client
limits:
timeout: 180000
memory: 512
triggers:
data-arrival:
feed: /whisk.system/alarms/interval
inputs:
minutes: 5
rules:
process-periodically:
trigger: data-arrival
action: java-demo/process-and-store

Deployment Script

deploy/deploy.sh

#!/bin/bash
set -e
# Configuration
WHISK_API_HOST=${WHISK_API_HOST:-https://localhost:31001}
WHISK_AUTH=${WHISK_AUTH:-23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP}
PACKAGE_NAME="java-demo"
echo "Deploying OpenWhisk Java actions to $WHISK_API_HOST"
# Build the project
echo "Building Java project..."
mvn clean package -DskipTests
# Create package
echo "Creating package: $PACKAGE_NAME"
wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \
package update "$PACKAGE_NAME" \
--param version "1.0.0" \
--param description "Java OpenWhisk Actions Demo"
# Deploy actions
deploy_action() {
local action_name=$1
local jar_file=$2
local memory=$3
local timeout=$4
echo "Deploying action: $action_name"
wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \
action update "$PACKAGE_NAME/$action_name" \
"$jar_file" \
--main com.example.openwhisk.Main \
--kind java:8 \
--memory "$memory" \
--timeout "$timeout" \
--web true
}
# Deploy individual actions
deploy_action "hello-world" "target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar" 256 30000
deploy_action "json-processor" "target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar" 512 60000
deploy_action "http-client" "target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar" 512 120000
# Create sequence
echo "Creating sequence: process-and-store"
wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \
action update "$PACKAGE_NAME/process-and-store" \
--sequence "$PACKAGE_NAME/json-processor","$PACKAGE_NAME/http-client" \
--web true
echo "Deployment completed successfully!"
echo ""
echo "Test endpoints:"
echo "  Hello World: $WHISK_API_HOST/api/v1/web/$(echo $WHISK_AUTH | cut -d: -f1)/$PACKAGE_NAME/hello-world.json"
echo "  JSON Processor: $WHISK_API_HOST/api/v1/web/$(echo $WHISK_AUTH | cut -d: -f1)/$PACKAGE_NAME/json-processor.json"

Step 7: Testing

Unit Tests

HelloWorldActionTest.java

package com.example.openwhisk.actions;
import com.example.openwhisk.core.ActionException;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class HelloWorldActionTest {
private final HelloWorldAction action = new HelloWorldAction();
@Test
void testExecuteWithName() {
Map<String, Object> input = Map.of("name", "John");
Map<String, Object> result = action.execute(input);
assertNotNull(result);
assertTrue(result.containsKey("data"));
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) result.get("data");
assertEquals("Hello, John!", data.get("message"));
}
@Test
void testExecuteWithCustomGreeting() {
Map<String, Object> input = Map.of("name", "Jane", "greeting", "Hi");
Map<String, Object> result = action.execute(input);
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) result.get("data");
assertEquals("Hi, Jane!", data.get("message"));
}
@Test
void testExecuteWithEmptyInput() {
Map<String, Object> input = Map.of();
Map<String, Object> result = action.execute(input);
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) result.get("data");
assertEquals("Hello, World!", data.get("message"));
}
@Test
void testValidateInputWithLongName() {
Map<String, Object> input = Map.of("name", "A".repeat(101));
ActionException exception = assertThrows(ActionException.class, 
() -> action.validateInput(input));
assertEquals("Name too long", exception.getMessage());
assertEquals(400, exception.getStatusCode());
}
}

Integration Test

MainIntegrationTest.java

package com.example.openwhisk;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class MainIntegrationTest {
@Test
void testMainWithHelloAction() {
Map<String, Object> input = Map.of(
"name", "TestUser",
"greeting", "Welcome"
);
Map<String, Object> result = Main.testMain(input);
assertNotNull(result);
assertTrue(result.containsKey("data"));
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) result.get("data");
assertEquals("Welcome, TestUser!", data.get("message"));
}
@Test
void testMainWithJsonAction() {
Map<String, Object> input = Map.of(
"data", "{\"name\": \"John\", \"age\": 30}",
"operation", "validate"
);
Map<String, Object> result = Main.testMain(input);
assertNotNull(result);
assertTrue(result.containsKey("data"));
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) result.get("data");
assertEquals("validate", data.get("operation"));
assertTrue((Boolean) data.get("success"));
}
@Test
void testMainWithInvalidAction() {
Map<String, Object> input = Map.of(
"invalid", "parameter"
);
Map<String, Object> result = Main.testMain(input);
assertNotNull(result);
assertTrue((Boolean) result.get("error"));
assertEquals("ROUTING_ERROR", result.get("code"));
}
}

Test Script for OpenWhisk

test/test-actions.sh

#!/bin/bash
set -e
# Configuration
WHISK_API_HOST=${WHISK_API_HOST:-https://localhost:31001}
WHISK_AUTH=${WHISK_AUTH:-23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP}
PACKAGE_NAME="java-demo"
echo "Testing OpenWhisk Java actions on $WHISK_API_HOST"
test_action() {
local action_name=$1
local input_file=$2
local expected_field=$3
local expected_value=$4
echo "Testing action: $action_name"
result=$(wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \
action invoke "$PACKAGE_NAME/$action_name" \
--param-file "$input_file" \
--result)
if echo "$result" | jq -e ".${expected_field} == \"${expected_value}\"" > /dev/null; then
echo "✅ $action_name test passed"
else
echo "❌ $action_name test failed"
echo "Result: $result"
exit 1
fi
}
# Test hello-world action
cat > /tmp/hello-input.json << EOF
{
"name": "TestUser",
"greeting": "Hello"
}
EOF
test_action "hello-world" "/tmp/hello-input.json" "data.message" "Hello, TestUser!"
# Test json-processor action
cat > /tmp/json-input.json << EOF
{
"data": "{\"name\": \"John\", \"age\": 30}",
"operation": "validate"
}
EOF
test_action "json-processor" "/tmp/json-input.json" "success" "true"
echo "All tests passed! 🎉"

Best Practices

1. Performance Optimization

  • Use connection pooling for database actions
  • Implement caching for frequently accessed data
  • Keep actions stateless for better scalability
  • Use appropriate memory and timeout settings

2. Error Handling

  • Implement comprehensive input validation
  • Use meaningful error codes and messages
  • Log errors for debugging
  • Implement retry logic for transient failures

3. Security

  • Validate all input parameters
  • Use environment variables for sensitive data
  • Implement proper authentication and authorization
  • Sanitize output to prevent injection attacks

4. Monitoring and Logging

  • Log action execution times
  • Monitor memory usage and cold start times
  • Implement health checks
  • Use structured logging for better analysis

Conclusion

This comprehensive OpenWhisk Java actions implementation provides:

  • Modular Architecture: Clean separation of concerns with actions, factories, and routers
  • Type Safety: Strongly typed configuration and input validation
  • Extensibility: Easy to add new actions by implementing the Action interface
  • Production Ready: Includes error handling, logging, and monitoring
  • Deployment Ready: Complete Docker and OpenWhisk deployment configuration

Key Benefits:

  1. Serverless Efficiency: Pay only for execution time
  2. Java Ecosystem: Leverage existing Java libraries and tools
  3. Scalability: Automatic scaling based on demand
  4. Event-Driven: Perfect for microservices and event processing
  5. Cost Effective: No infrastructure management required

By implementing this solution, you can build robust, scalable serverless applications using Java that integrate seamlessly with the OpenWhisk platform.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper