From ece1fbd1a82f006c102fbeb1712052a133c46891 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 5 May 2026 14:02:06 +0200 Subject: [PATCH] Add HTTP server example using Netty and PerlOnJava This example demonstrates how to create a web server where HTTP request handlers are written in Perl, with Netty handling the networking layer. Features: - Complete working HTTP server with Netty - Request handlers written in Perl (handler.pl) - Multiple endpoints: home, API, forms, time, env, echo - Thread-safe implementation using single event loop - Comprehensive documentation with architecture diagrams - Makefile with automatic dependency download - Automated test script - Proper separation between Java (HTTP) and Perl (business logic) Technical implementation: - Uses RuntimeHash.createHashRef() for proper hash reference passing - Demonstrates calling Perl subroutines from Java - Shows how to bridge between Java objects and Perl data structures - Documents thread safety considerations for PerlOnJava Files: - HttpServerExample.java: Netty server that calls Perl handlers - handler.pl: Perl request handlers with routing logic - Makefile: Build system with deps, compile, run, test targets - README.md: Comprehensive documentation and debugging tips - test.sh: Automated endpoint testing - .gitignore: Excludes compiled files and downloaded libraries Usage: cd examples/http_server make run Then visit http://localhost:8080 in a browser or: curl http://localhost:8080/ curl http://localhost:8080/api/users make test Co-Authored-By: Claude Sonnet 4.5 --- examples/http_server/.gitignore | 10 + examples/http_server/HttpServerExample.java | 229 ++++++++++++ examples/http_server/Makefile | 126 +++++++ examples/http_server/README.md | 340 +++++++++++++++++ examples/http_server/handler.pl | 394 ++++++++++++++++++++ examples/http_server/test.sh | 78 ++++ 6 files changed, 1177 insertions(+) create mode 100644 examples/http_server/.gitignore create mode 100644 examples/http_server/HttpServerExample.java create mode 100644 examples/http_server/Makefile create mode 100644 examples/http_server/README.md create mode 100644 examples/http_server/handler.pl create mode 100755 examples/http_server/test.sh diff --git a/examples/http_server/.gitignore b/examples/http_server/.gitignore new file mode 100644 index 000000000..bc8d45b04 --- /dev/null +++ b/examples/http_server/.gitignore @@ -0,0 +1,10 @@ +# Compiled Java classes +*.class + +# Library directory (downloaded dependencies) +lib/ + +# IDE files +.idea/ +*.iml +.vscode/ diff --git a/examples/http_server/HttpServerExample.java b/examples/http_server/HttpServerExample.java new file mode 100644 index 000000000..509f19a94 --- /dev/null +++ b/examples/http_server/HttpServerExample.java @@ -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() { + @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 { + + @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(); + } + } +} diff --git a/examples/http_server/Makefile b/examples/http_server/Makefile new file mode 100644 index 000000000..3db7ca2a6 --- /dev/null +++ b/examples/http_server/Makefile @@ -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)" diff --git a/examples/http_server/README.md b/examples/http_server/README.md new file mode 100644 index 000000000..db0e7a4b4 --- /dev/null +++ b/examples/http_server/README.md @@ -0,0 +1,340 @@ +# PerlOnJava HTTP Server Example + +A complete HTTP web server example using **Netty** and **PerlOnJava**, where request handlers are written in Perl for easy customization. + +## Overview + +This example demonstrates how to: + +- Create a high-performance web server using Netty's async I/O +- Write request handlers in Perl (loaded from external files) +- Handle thread safety in PerlOnJava (currently single-threaded event loop) +- Route HTTP requests and return HTML/JSON responses +- Bridge between Java (Netty) and Perl (request handlers) + +## Architecture + +``` +┌─────────────┐ +│ Browser │ +└──────┬──────┘ + │ HTTP Request + ▼ +┌──────────────────────────────┐ +│ Netty HTTP Server (Java) │ +│ - Single event loop thread │ +│ - Handles concurrent I/O │ +└──────┬───────────────────────┘ + │ Call handle_request() + ▼ +┌──────────────────────────────┐ +│ Perl Handler (handler.pl) │ +│ - Routes requests │ +│ - Generates responses │ +└──────┬───────────────────────┘ + │ Return response hash + ▼ +┌──────────────────────────────┐ +│ Netty sends HTTP response │ +└──────────────────────────────┘ +``` + +## Thread Safety + +**IMPORTANT:** PerlOnJava is currently **NOT thread-safe**. All global Perl state (variables, arrays, hashes) is stored in static fields without synchronization. + +This example uses a **single-threaded event loop** to avoid race conditions: + +- ✅ Only one thread accesses Perl state +- ✅ Multiple concurrent connections supported (via async I/O) +- ✅ No locks needed +- ❌ CPU-bound handlers can block other requests + +**Alternative approach** (not used here): +- Use a global lock: `synchronized(PERL_LOCK) { ... }` +- This serializes all requests but works with multi-threaded event loops + +**Future:** 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. **Java 22+** - Required by PerlOnJava +2. **Build PerlOnJava** - The fat JAR must be built first +3. **curl** - For testing (optional) + +## Quick Start + +### 1. Build PerlOnJava + +```bash +# From the project root +make +# or: ./gradlew shadowJar +``` + +This creates `target/perlonjava-5.42.0.jar`. + +### 2. Build and Run the Server + +```bash +cd examples/http_server +make run +``` + +This will: +- Download Netty JAR (first time only) +- Compile `HttpServerExample.java` +- Start the server on `http://localhost:8080` + +### 3. Test the Server + +Open your browser and visit: +- http://localhost:8080/ - Home page with endpoint list +- http://localhost:8080/api/users - JSON API example +- http://localhost:8080/time - Current server time +- http://localhost:8080/env - Request environment info +- http://localhost:8080/echo?message=Hello&count=3 - Echo service + +Or use curl: +```bash +# Home page +curl http://localhost:8080/ + +# API endpoint +curl http://localhost:8080/api/users + +# POST form data +curl -X POST http://localhost:8080/form -d "name=Alice&age=30" + +# Echo with query params +curl "http://localhost:8080/echo?message=Hello%20World&count=3" +``` + +Or run the automated tests: +```bash +# In another terminal +make test +``` + +## File Structure + +``` +examples/http_server/ +├── HttpServerExample.java # Java/Netty server code +├── handler.pl # Perl request handler +├── Makefile # Build and run commands +└── README.md # This file +``` + +## Customizing the Perl Handler + +The Perl handler in `handler.pl` receives a request hash: + +```perl +sub handle_request { + my ($request) = @_; + + # Request structure: + # { + # method => 'GET', + # path => '/api/users', + # uri => '/api/users?id=123', + # body => 'request body', + # query => { id => '123' }, + # headers => { 'content-type' => 'application/json' } + # } + + # Return a response hash: + return { + status => 200, + content_type => 'text/html', + body => '

Hello from Perl!

' + }; +} +``` + +### Adding New Routes + +Edit `handler.pl` and add your route: + +```perl +sub handle_request { + my ($request) = @_; + my $path = $request->{path}; + + if ($path eq '/my-new-page') { + return { + status => 200, + content_type => 'text/html', + body => '

My New Page

' + }; + } + # ... existing routes +} +``` + +The server will automatically reload when you restart it (no Java recompilation needed). + +## Makefile Targets + +```bash +make # Download dependencies and compile +make deps # Download Netty JAR only +make compile # Compile Java source only +make run # Run server on port 8080 +make run-port PORT=9000 # Run on custom port +make test # Test endpoints with curl +make clean # Remove compiled files and dependencies +make help # Show help message +``` + +## Running on a Different Port + +```bash +make run-port PORT=9000 +``` + +Or directly: +```bash +java --enable-native-access=ALL-UNNAMED \ + -cp "../../target/perlonjava-5.42.0.jar:lib/*:." \ + examples.http_server.HttpServerExample 9000 +``` + +## How It Works + +1. **Initialization** + - PerlOnJava runtime is initialized + - `handler.pl` is loaded and compiled + - Reference to `handle_request()` subroutine is obtained + +2. **Request Handling** + - Netty receives HTTP request + - Request data is converted to Perl hash structure + - `handle_request(\%request)` is called + - Perl handler returns response hash + - Netty sends HTTP response to client + +3. **Thread Safety** + - Single event loop thread handles all requests + - Netty uses async I/O for concurrent connections + - No Perl state shared between "requests" (but all requests use same runtime) + +## Performance Considerations + +### Current Implementation (Single Thread) + +- ✅ **Good for:** I/O-bound apps (database queries, API calls, file reading) +- ✅ **Handles:** Thousands of concurrent connections efficiently +- ❌ **Bad for:** CPU-intensive tasks (long computations, image processing) + +### When Multiplicity is Available + +Once PerlOnJava implements runtime isolation (see `dev/design/concurrency.md`), you'll be able to use a **runtime pool**: + +```java +PerlRuntimePool pool = new PerlRuntimePool(10); // 10 isolated runtimes + +// In your handler: +PerlRuntime rt = pool.acquire(); +try { + PerlRuntime.setCurrent(rt); + result = RuntimeCode.apply(handler, args, RuntimeContextType.SCALAR); +} finally { + pool.release(rt); +} +``` + +This enables true parallel request processing across CPU cores. + +## Troubleshooting + +### Error: PerlOnJava JAR not found + +```bash +cd ../.. +make +# or: ./gradlew shadowJar +``` + +### Error: Port already in use + +```bash +# Use a different port +make run-port PORT=9090 +``` + +Or kill the process using port 8080: +```bash +# macOS/Linux +lsof -ti:8080 | xargs kill -9 +``` + +### Error: Java version + +Ensure you have Java 22+: +```bash +java -version +``` + +### Connection refused when testing + +Make sure the server is running: +```bash +# Terminal 1: Start server +make run + +# Terminal 2: Test +make test +``` + +## Example Output + +``` +$ make run +Starting HTTP server on port 8080... + +======================================================= + HTTP Server started on http://localhost:8080 + Thread model: Single event loop (thread-safe) + Press Ctrl+C to stop +======================================================= +``` + +## Debugging Tips + +### Using --disassemble to Understand Internal APIs + +You can see how PerlOnJava handles Perl constructs internally by using the `--disassemble` flag: + +```bash +# See how hash references are created +./jperl --disassemble -e 'my $hash = { foo => "bar" }; print $hash->{foo};' 2>&1 | grep -A5 createHashRef +``` + +This shows that PerlOnJava uses `RuntimeHash.createHashRef()` to create hash references, which is why we use: + +```java +RuntimeScalar hashRef = RuntimeHash.createHashRef(request); +``` + +instead of manually constructing the scalar. This approach ensures compatibility with PerlOnJava's internal representation. + +## Related Examples + +- `examples/ExifToolExample.java` - Calling Perl modules from Java +- `examples/http.pl` - HTTP client using HTTP::Tiny + +## Further Reading + +- [Netty Documentation](https://netty.io/wiki/) +- [PerlOnJava Concurrency Design](../../dev/design/concurrency.md) +- [PerlOnJava Features](../../docs/reference/feature-matrix.md) + +## License + +This example is part of the PerlOnJava project and follows the same license. + +## Contributing + +Found a bug or want to improve this example? Please submit an issue or PR to the [PerlOnJava repository](https://github.com/fglock/PerlOnJava). diff --git a/examples/http_server/handler.pl b/examples/http_server/handler.pl new file mode 100644 index 000000000..78c37cfd0 --- /dev/null +++ b/examples/http_server/handler.pl @@ -0,0 +1,394 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +# HTTP Request Handler for PerlOnJava Web Server +# +# This subroutine is called for every HTTP request. +# It receives a hash reference with request information and +# returns a hash reference with the response. +# +# Request hash structure: +# { +# method => 'GET' | 'POST' | 'PUT' | etc. +# path => '/api/users' (without query string) +# uri => '/api/users?id=123' (full URI) +# body => 'request body content' +# query => { param1 => 'value1', ... } +# headers => { 'content-type' => 'text/html', ... } +# } +# +# Response hash structure: +# { +# status => 200, +# content_type => 'text/html', +# body => '...' +# } + +sub handle_request { + my ($request) = @_; + + my $method = $request->{method}; + my $path = $request->{path}; + my $query = $request->{query}; + + # Route dispatcher + if ($path eq '/') { + return handle_home($request); + } + elsif ($path =~ m{^/api/(.+)}) { + return handle_api($1, $request); + } + elsif ($path eq '/form') { + return handle_form($request); + } + elsif ($path eq '/time') { + return handle_time($request); + } + elsif ($path eq '/env') { + return handle_env($request); + } + elsif ($path eq '/echo') { + return handle_echo($request); + } + else { + return handle_404($path); + } +} + +# Home page handler +sub handle_home { + my ($request) = @_; + + my $html = <<'HTML'; + + + + PerlOnJava HTTP Server + + + +

Welcome to PerlOnJava HTTP Server!

+

This web server is powered by Netty and PerlOnJava.

+

Request handlers are written in Perl for easy customization.

+ +

Available Endpoints:

+ +
+ GET /
+ This page (home) +
+ +
+ GET /api/<resource>
+ API endpoint example
+ Try: /api/users, /api/products +
+ +
+ POST /form
+ Form submission example
+ curl -X POST http://localhost:8080/form -d "name=Alice&age=30" +
+ +
+ GET /time
+ Current server time
+ Click here +
+ +
+ GET /env
+ Server environment information
+ Click here +
+ +
+ GET /echo?message=hello
+ Echo back query parameters
+ Try it +
+ +
+

Powered by PerlOnJava 5.42.0 | Thread-safe via single event loop

+ + +HTML + + return { + status => 200, + content_type => 'text/html', + body => $html + }; +} + +# API handler +sub handle_api { + my ($resource, $request) = @_; + + # Simulate an API response + my $response_data; + + if ($resource eq 'users') { + $response_data = { + users => [ + { id => 1, name => 'Alice', email => 'alice@example.com' }, + { id => 2, name => 'Bob', email => 'bob@example.com' }, + { id => 3, name => 'Charlie', email => 'charlie@example.com' } + ] + }; + } + elsif ($resource eq 'products') { + $response_data = { + products => [ + { id => 101, name => 'Widget', price => 19.99 }, + { id => 102, name => 'Gadget', price => 29.99 }, + { id => 103, name => 'Doohickey', price => 9.99 } + ] + }; + } + else { + return { + status => 404, + content_type => 'application/json', + body => qq({"error":"Resource not found: $resource"}) + }; + } + + # Simple JSON serialization (for a real app, use JSON::PP or similar) + require Data::Dumper; + my $json = format_as_json($response_data); + + return { + status => 200, + content_type => 'application/json', + body => $json + }; +} + +# Form handler +sub handle_form { + my ($request) = @_; + + if ($request->{method} ne 'POST') { + return { + status => 405, + content_type => 'text/plain', + body => 'Method Not Allowed. Use POST.' + }; + } + + my $body = $request->{body}; + + # Parse form data (simple implementation) + my %form_data; + foreach my $pair (split /&/, $body) { + my ($key, $value) = split /=/, $pair, 2; + $key = uri_unescape($key); + $value = uri_unescape($value); + $form_data{$key} = $value; + } + + my $html = "

Form Received

";
+    foreach my $key (sort keys %form_data) {
+        $html .= "$key = $form_data{$key}\n";
+    }
+    $html .= "
"; + + return { + status => 200, + content_type => 'text/html', + body => $html + }; +} + +# Time handler +sub handle_time { + my ($request) = @_; + + my $time = localtime(); + my $epoch = time(); + + my $html = < + +Server Time + +

Current Server Time

+

Local Time: $time

+

Unix Epoch: $epoch

+

Back to Home

+ + +HTML + + return { + status => 200, + content_type => 'text/html', + body => $html + }; +} + +# Environment handler +sub handle_env { + my ($request) = @_; + + my $html = <<'HTML'; + + +Environment + +

Server Environment

+

Request Information

+
+HTML
+
+    $html .= "Method: " . $request->{method} . "\n";
+    $html .= "Path: " . $request->{path} . "\n";
+    $html .= "URI: " . $request->{uri} . "\n";
+
+    $html .= "\nHeaders:\n";
+    foreach my $key (sort keys %{$request->{headers}}) {
+        $html .= "  $key: " . $request->{headers}{$key} . "\n";
+    }
+
+    if (%{$request->{query}}) {
+        $html .= "\nQuery Parameters:\n";
+        foreach my $key (sort keys %{$request->{query}}) {
+            $html .= "  $key: " . $request->{query}{$key} . "\n";
+        }
+    }
+
+    $html .= <<'HTML';
+    
+

Perl Version

+
+HTML
+
+    $html .= "Perl Version: $]\n";
+    $html .= "Platform: $^O\n";
+
+    $html .= <<'HTML';
+    
+

Back to Home

+ + +HTML + + return { + status => 200, + content_type => 'text/html', + body => $html + }; +} + +# Echo handler +sub handle_echo { + my ($request) = @_; + + my $query = $request->{query}; + my $message = $query->{message} || 'No message provided'; + my $count = $query->{count} || 1; + + my $html = < + +Echo + +

Echo Service

+HTML + + for (my $i = 1; $i <= $count; $i++) { + $html .= "

$i: $message

\n"; + } + + $html .= <<'HTML'; +

Back to Home

+ + +HTML + + return { + status => 200, + content_type => 'text/html', + body => $html + }; +} + +# 404 handler +sub handle_404 { + my ($path) = @_; + + my $html = < + +404 Not Found + +

404 - Not Found

+

The requested path $path was not found on this server.

+

Back to Home

+ + +HTML + + return { + status => 404, + content_type => 'text/html', + body => $html + }; +} + +# Helper: Simple JSON formatter (for demo purposes) +sub format_as_json { + my ($data) = @_; + my $json = to_json($data); + return $json; +} + +sub to_json { + my ($val) = @_; + + if (ref($val) eq 'HASH') { + my @pairs; + foreach my $key (keys %$val) { + push @pairs, qq("$key":) . to_json($val->{$key}); + } + return '{' . join(',', @pairs) . '}'; + } + elsif (ref($val) eq 'ARRAY') { + my @items; + foreach my $item (@$val) { + push @items, to_json($item); + } + return '[' . join(',', @items) . ']'; + } + elsif (looks_like_number($val)) { + return $val; + } + else { + $val =~ s/\\/\\\\/g; + $val =~ s/"/\\"/g; + return qq("$val"); + } +} + +sub looks_like_number { + my ($val) = @_; + return $val =~ /^-?\d+(\.\d+)?$/; +} + +# Helper: Simple URL unescape +sub uri_unescape { + my ($str) = @_; + $str =~ s/\+/ /g; + $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; + return $str; +} + +# Return true to indicate successful loading +1; diff --git a/examples/http_server/test.sh b/examples/http_server/test.sh new file mode 100755 index 000000000..1607dd009 --- /dev/null +++ b/examples/http_server/test.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Test script for PerlOnJava HTTP Server +# This script tests all endpoints and verifies responses + +set -e + +SERVER_URL="${SERVER_URL:-http://localhost:8080}" +FAILED=0 + +echo "Testing PerlOnJava HTTP Server at $SERVER_URL" +echo "================================================" +echo "" + +# Function to test an endpoint +test_endpoint() { + local method="$1" + local path="$2" + local data="$3" + local expected="$4" + local description="$5" + + echo -n "Testing: $description ... " + + if [ -n "$data" ]; then + response=$(curl -s -X "$method" "$SERVER_URL$path" -d "$data") + else + response=$(curl -s -X "$method" "$SERVER_URL$path") + fi + + if echo "$response" | grep -q "$expected"; then + echo "✓ PASS" + return 0 + else + echo "✗ FAIL" + echo " Expected to find: $expected" + echo " Got: ${response:0:100}..." + FAILED=$((FAILED + 1)) + return 1 + fi +} + +# Wait for server to be ready +echo "Checking if server is running..." +for i in {1..10}; do + if curl -s "$SERVER_URL/" > /dev/null 2>&1; then + echo "Server is ready!" + echo "" + break + fi + if [ $i -eq 10 ]; then + echo "Error: Server is not responding at $SERVER_URL" + echo "Make sure the server is running: make run" + exit 1 + fi + sleep 1 +done + +# Run tests +test_endpoint "GET" "/" "" "PerlOnJava HTTP Server" "Home page" +test_endpoint "GET" "/api/users" "" "Alice" "API - users" +test_endpoint "GET" "/api/products" "" "Widget" "API - products" +test_endpoint "GET" "/api/unknown" "" "Resource not found" "API - 404" +test_endpoint "POST" "/form" "name=Alice&age=30" "Alice" "POST form" +test_endpoint "GET" "/form" "" "Method Not Allowed" "Form - wrong method" +test_endpoint "GET" "/time" "" "Local Time" "Time endpoint" +test_endpoint "GET" "/env" "" "Request Information" "Environment info" +test_endpoint "GET" "/echo?message=Hello&count=2" "" "1: Hello" "Echo service" +test_endpoint "GET" "/notfound" "" "404 - Not Found" "404 handler" + +echo "" +echo "================================================" +if [ $FAILED -eq 0 ]; then + echo "All tests passed! ✓" + exit 0 +else + echo "$FAILED test(s) failed ✗" + exit 1 +fi