Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/http_server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Compiled Java classes
*.class

# Library directory (downloaded dependencies)
lib/

# IDE files
.idea/
*.iml
.vscode/
229 changes: 229 additions & 0 deletions examples/http_server/HttpServerExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package examples.http_server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.perlonjava.app.cli.CompilerOptions;
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import org.perlonjava.runtime.runtimetypes.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
* HTTP Server Example using Netty and PerlOnJava.
*
* This example demonstrates how to create a web server where request handlers
* are written in Perl. The Perl handler is loaded from a separate file for
* better code organization.
*
* IMPORTANT: Thread Safety Considerations
*
* PerlOnJava is currently NOT thread-safe. All global state (variables, arrays,
* hashes) is stored in static fields without synchronization. This example uses
* a SINGLE-THREADED event loop to avoid race conditions:
*
* - All requests are handled by one event loop thread
* - Multiple concurrent connections are supported (via async I/O)
* - No locks needed since only one thread accesses Perl state
*
* Alternative approach (not used here):
* - Use a global lock: synchronized(PERL_LOCK) { ... }
* - This serializes all requests but works with multi-threaded event loops
*
* Future: PerlRuntime Pool
* Once multiplicity is implemented (see dev/design/concurrency.md), you can
* use a pool of isolated Perl runtimes for true parallel request handling.
*
* Prerequisites:
* 1. Build the fat jar:
* make
* or:
* ./gradlew shadowJar
* 2. Download Netty JARs (this will be done by the Makefile)
*
* Run:
* make run
*
* Test:
* curl http://localhost:8080/
* curl http://localhost:8080/api/users
* curl -X POST http://localhost:8080/form -d "name=Alice"
*/
public class HttpServerExample {

private static RuntimeScalar perlHandler;

public static void main(String[] args) throws Exception {
int port = 8080;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}

System.out.println("Initializing PerlOnJava...");
initializePerlHandler();
System.out.println("Perl handler loaded successfully.");

// Use a SINGLE event loop thread to avoid thread-safety issues
// This still handles many concurrent connections efficiently via async I/O
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(1); // Single worker thread

try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new HttpObjectAggregator(65536));
ch.pipeline().addLast(new HttpRequestHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);

ChannelFuture f = b.bind(port).sync();
System.out.println("\n" +
"=======================================================\n" +
" HTTP Server started on http://localhost:" + port + "\n" +
" Thread model: Single event loop (thread-safe)\n" +
" Press Ctrl+C to stop\n" +
"=======================================================\n");

f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}

/**
* Initialize PerlOnJava and load the Perl handler from handler.pl
*/
private static void initializePerlHandler() throws Exception {
// Initialize PerlOnJava runtime
PerlLanguageProvider.resetAll();

// Read the Perl handler script from file
String handlerPath = "examples/http_server/handler.pl";
String perlCode = new String(Files.readAllBytes(Paths.get(handlerPath)));

// Compile and execute the Perl script
CompilerOptions options = new CompilerOptions();
options.fileName = handlerPath;
options.code = perlCode;

PerlLanguageProvider.executePerlCode(options, true);

// Get reference to the handle_request subroutine
perlHandler = GlobalVariable.getGlobalCodeRef("main::handle_request");

if (perlHandler == null || perlHandler.value == null) {
throw new RuntimeException(
"Failed to load handle_request subroutine from " + handlerPath);
}
}

/**
* Netty handler that processes HTTP requests by calling Perl code
*/
static class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
// Extract request information
String method = req.method().name();
String uri = req.uri();
String body = req.content().toString(CharsetUtil.UTF_8);

// Build query parameters hash
RuntimeHash queryParams = new RuntimeHash();
QueryStringDecoder queryDecoder = new QueryStringDecoder(uri);
queryDecoder.parameters().forEach((key, values) -> {
if (!values.isEmpty()) {
queryParams.put(key, new RuntimeScalar(values.get(0)));
}
});

// Build headers hash
RuntimeHash headers = new RuntimeHash();
req.headers().forEach(entry -> {
headers.put(entry.getKey().toLowerCase(), new RuntimeScalar(entry.getValue()));
});

// Build request hash
RuntimeHash request = new RuntimeHash();
request.put("method", new RuntimeScalar(method));
request.put("path", new RuntimeScalar(queryDecoder.path()));
request.put("uri", new RuntimeScalar(uri));
request.put("body", new RuntimeScalar(body));
request.put("query", new RuntimeScalar(queryParams));
request.put("headers", new RuntimeScalar(headers));

// Call Perl handler: handle_request(\%request)
// Create a proper hash reference using the PerlOnJava API
RuntimeScalar hashRef = RuntimeHash.createHashRef(request);

RuntimeArray args = new RuntimeArray();
RuntimeArray.push(args, hashRef);

RuntimeList resultList;
try {
resultList = RuntimeCode.apply(perlHandler, args, RuntimeContextType.SCALAR);
} catch (Exception e) {
// Error in Perl handler
e.printStackTrace();
sendResponse(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR,
"text/plain", "Internal Server Error: " + e.getMessage());
return;
}

// Get the scalar result from the list
RuntimeScalar resultScalar = resultList.scalar();

// Parse response from Perl
if (resultScalar.value instanceof RuntimeHash) {
RuntimeHash response = resultScalar.hashDeref();

int status = response.get("status").getInt();
String contentType = response.get("content_type").toString();
String responseBody = response.get("body").toString();

HttpResponseStatus httpStatus = HttpResponseStatus.valueOf(status);
sendResponse(ctx, httpStatus, contentType, responseBody);
} else {
// Perl handler returned a simple string
sendResponse(ctx, HttpResponseStatus.OK, "text/plain", resultScalar.toString());
}
}

private void sendResponse(ChannelHandlerContext ctx, HttpResponseStatus status,
String contentType, String body) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
status,
Unpooled.copiedBuffer(body, CharsetUtil.UTF_8)
);

response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=utf-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);

ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
}
126 changes: 126 additions & 0 deletions examples/http_server/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Makefile for PerlOnJava HTTP Server Example
#
# This example demonstrates a web server using Netty and PerlOnJava where
# HTTP request handlers are written in Perl.

# Configuration
JAVA_VERSION = 22
NETTY_VERSION = 4.1.115.Final
PERLONJAVA_JAR = target/perlonjava-5.42.0.jar
PERLONJAVA_JAR_LOCAL = ../../target/perlonjava-5.42.0.jar
LIB_DIR = lib
NETTY_JARS = \
$(LIB_DIR)/netty-common-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-buffer-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-transport-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-codec-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-codec-http-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-handler-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-resolver-$(NETTY_VERSION).jar \
$(LIB_DIR)/netty-transport-classes-epoll-$(NETTY_VERSION).jar

# Java compiler and runtime flags
JAVAC = javac
JAVA = java
JAVAC_FLAGS = -cp "$(PERLONJAVA_JAR_LOCAL):$(LIB_DIR)/*"
JAVA_FLAGS = --enable-native-access=ALL-UNNAMED -cp "$(PERLONJAVA_JAR):examples/http_server/$(LIB_DIR)/*:."

# Targets
.PHONY: all clean deps compile run test help

# Default target
all: deps compile

# Download Netty dependencies
deps:
@echo "Checking dependencies..."
@mkdir -p $(LIB_DIR)
@for jar in netty-common netty-buffer netty-transport netty-codec netty-codec-http netty-handler netty-resolver netty-transport-classes-epoll; do \
if [ ! -f "$(LIB_DIR)/$$jar-$(NETTY_VERSION).jar" ]; then \
echo "Downloading $$jar-$(NETTY_VERSION).jar..."; \
curl -f -L -o "$(LIB_DIR)/$$jar-$(NETTY_VERSION).jar" \
"https://repo1.maven.org/maven2/io/netty/$$jar/$(NETTY_VERSION)/$$jar-$(NETTY_VERSION).jar" || \
echo "Warning: Failed to download $$jar (may not be needed)"; \
fi; \
done
@echo "Dependencies ready."

# Ensure PerlOnJava JAR exists
check-jar:
@if [ ! -f "$(PERLONJAVA_JAR_LOCAL)" ]; then \
echo "Error: PerlOnJava JAR not found at $(PERLONJAVA_JAR_LOCAL)"; \
echo "Please build it first:"; \
echo " cd ../.. && make"; \
echo " or: ./gradlew shadowJar"; \
exit 1; \
fi

# Compile the Java source
compile: check-jar deps
@echo "Compiling HttpServerExample.java..."
$(JAVAC) $(JAVAC_FLAGS) HttpServerExample.java

# Run the HTTP server
run: compile
@echo "Starting HTTP server on port 8080..."
@echo ""
cd ../.. && $(JAVA) $(JAVA_FLAGS) examples.http_server.HttpServerExample

# Run on a custom port
run-port: compile
@echo "Starting HTTP server on port $(PORT)..."
@echo ""
cd ../.. && $(JAVA) $(JAVA_FLAGS) examples.http_server.HttpServerExample $(PORT)

# Test the server with curl
test:
@echo "Testing HTTP server endpoints..."
@echo ""
@echo "=== GET / (Home) ==="
@curl -s http://localhost:8080/ | head -20
@echo ""
@echo ""
@echo "=== GET /api/users (API) ==="
@curl -s http://localhost:8080/api/users
@echo ""
@echo ""
@echo "=== GET /time ==="
@curl -s http://localhost:8080/time | grep -A2 "Local Time"
@echo ""
@echo ""
@echo "=== POST /form ==="
@curl -s -X POST http://localhost:8080/form -d "name=Alice&age=30" | grep -A5 "Form Received"
@echo ""
@echo ""
@echo "=== GET /echo ==="
@curl -s "http://localhost:8080/echo?message=Hello&count=2" | grep -A3 "Echo Service"
@echo ""
@echo ""
@echo "Done!"

# Clean compiled files
clean:
@echo "Cleaning up..."
rm -f HttpServerExample.class
rm -f HttpServerExample$$HttpRequestHandler.class
rm -rf $(LIB_DIR)

# Show help
help:
@echo "PerlOnJava HTTP Server Example - Makefile"
@echo ""
@echo "Available targets:"
@echo " make - Download dependencies and compile"
@echo " make deps - Download Netty JAR"
@echo " make compile - Compile Java source"
@echo " make run - Run the HTTP server (port 8080)"
@echo " make run-port PORT=9000 - Run on custom port"
@echo " make test - Test server endpoints with curl"
@echo " make clean - Remove compiled files and dependencies"
@echo " make help - Show this help message"
@echo ""
@echo "Usage:"
@echo " 1. Build PerlOnJava first: cd ../.. && make"
@echo " 2. Run server: cd examples/http_server && make run"
@echo " 3. Test: curl http://localhost:8080/"
@echo " or: make test (in another terminal)"
Loading
Loading