├── .gitignore ├── src ├── main │ ├── webapp │ │ └── .gitkeep │ ├── resources │ │ └── application.conf │ └── java │ │ └── com │ │ └── ofallonfamily │ │ └── jersey2akka │ │ ├── DoublingActor.java │ │ ├── ExampleApplication.java │ │ └── ExampleService.java └── test │ └── java │ └── com │ └── ofallonfamily │ └── jersey2akka │ ├── WebappDirectoryTest.java │ └── DoublerTest.java ├── .travis.yml ├── .github ├── dependabot.yml ├── workflows │ └── ci.yml └── copilot-instructions.md ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target -------------------------------------------------------------------------------- /src/main/webapp/.gitkeep: -------------------------------------------------------------------------------- 1 | # This file ensures the webapp directory is tracked by git 2 | # The webapp directory is required for Maven WAR packaging -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file has been replaced by GitHub Actions workflow at .github/workflows/ci.yml 2 | language: java 3 | sudo: false # faster builds 4 | 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # In this file you can override any option defined in the reference files. 2 | # Copy in parts of the reference files and modify as you please. 3 | 4 | akka { 5 | 6 | # Loggers to register at boot time (akka.event.Logging$DefaultLogger logs 7 | # to STDOUT) 8 | loggers = ["akka.event.Logging$DefaultLogger"] 9 | 10 | # Log level used by the configured loggers (see "loggers") as soon 11 | # as they have been started; before that, see "stdout-loglevel" 12 | # Options: OFF, ERROR, WARNING, INFO, DEBUG 13 | loglevel = "DEBUG" 14 | 15 | # Log level for the very basic logger activated during AkkaApplication startup 16 | # Options: OFF, ERROR, WARNING, INFO, DEBUG 17 | stdout-loglevel = "DEBUG" 18 | 19 | } -------------------------------------------------------------------------------- /src/test/java/com/ofallonfamily/jersey2akka/WebappDirectoryTest.java: -------------------------------------------------------------------------------- 1 | package com.ofallonfamily.jersey2akka; 2 | 3 | import org.junit.Test; 4 | import java.io.File; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | /** 8 | * Test to verify that the src/main/webapp directory exists. 9 | * This addresses the issue where the Maven Tomcat plugin was failing 10 | * with IllegalArgumentException due to missing webapp directory. 11 | */ 12 | public class WebappDirectoryTest { 13 | 14 | @Test 15 | public void testWebappDirectoryExists() { 16 | // Verify that src/main/webapp directory exists 17 | File webappDir = new File("src/main/webapp"); 18 | assertTrue("src/main/webapp directory should exist for WAR packaging", 19 | webappDir.exists() && webappDir.isDirectory()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/ofallonfamily/jersey2akka/DoublingActor.java: -------------------------------------------------------------------------------- 1 | package com.ofallonfamily.jersey2akka; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.actor.Props; 5 | import akka.event.Logging; 6 | import akka.event.LoggingAdapter; 7 | 8 | public class DoublingActor extends AbstractActor { 9 | 10 | LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this); 11 | 12 | public static Props mkProps() { 13 | return Props.create(DoublingActor.class); 14 | } 15 | 16 | @Override 17 | public void preStart() { 18 | log.debug("starting"); 19 | } 20 | 21 | @Override 22 | public Receive createReceive() { 23 | return receiveBuilder() 24 | .match(Integer.class, message -> { 25 | log.debug("received message: " + message); 26 | getSender().tell(message * 2, getSelf()); 27 | }) 28 | .matchAny(this::unhandled) 29 | .build(); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/test/java/com/ofallonfamily/jersey2akka/DoublerTest.java: -------------------------------------------------------------------------------- 1 | package com.ofallonfamily.jersey2akka; 2 | 3 | import org.glassfish.jersey.client.ClientConfig; 4 | import org.glassfish.jersey.test.JerseyTest; 5 | import org.junit.Test; 6 | 7 | import jakarta.ws.rs.core.Application; 8 | import jakarta.ws.rs.core.GenericType; 9 | import java.util.HashMap; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class DoublerTest extends JerseyTest { 14 | 15 | protected Application configure() { 16 | return new ExampleApplication(); 17 | } 18 | 19 | protected void configureClient(ClientConfig clientConfig) { 20 | // Jersey 3.x handles Jackson automatically 21 | } 22 | 23 | @Test 24 | public void testWithTwo() { 25 | 26 | HashMap map = target("doubler").path("2") 27 | .request().get(new GenericType>() {}); 28 | 29 | assertEquals(new Integer(4), map.get("results")); 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | 22 | - name: Cache Maven packages 23 | uses: actions/cache@v4 24 | with: 25 | path: ~/.m2 26 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 27 | restore-keys: ${{ runner.os }}-m2 28 | 29 | - name: Run tests and generate coverage 30 | run: mvn clean jacoco:prepare-agent test jacoco:report 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v4 34 | if: success() 35 | with: 36 | file: ./target/site/jacoco/jacoco.xml 37 | flags: unittests 38 | name: codecov-umbrella 39 | fail_ci_if_error: false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jersey2-akka-java 2 | ================= 3 | 4 | An example asynchronous REST API written in Java using Jersey 3 (🤷🏼‍♂️) and Akka 5 | 6 | [![CI](https://github.com/pofallon/jersey2-akka-java/workflows/CI/badge.svg)](https://github.com/pofallon/jersey2-akka-java/actions/workflows/ci.yml) 7 | [![Dependabot enabled](https://img.shields.io/badge/Dependabot-enabled-brightgreen.svg)](https://github.com/dependabot) 8 | 9 | Key concepts 10 | ------------ 11 | * Instantiating an Akka ActorSystem at server startup and injecting it into each request 12 | * Fulfilling an asynchronous Jersey REST service invocation using Akka actors 13 | 14 | How to run the example 15 | ---------------------- 16 | 1. Clone this repository 17 | 2. Run `mvn cargo:run` 18 | 3. Visit `http://localhost:9090/examples/doubler/2` via curl or your favorite browser 19 | 20 | The application uses the Cargo Maven plugin to run Apache Tomcat 10.1.43, which provides full support for Jakarta EE and modern Java versions. 21 | 22 | **Note:** The legacy `mvn tomcat7:run` command is still available but uses the older Tomcat 7.0.47 which has compatibility issues with modern Jakarta EE libraries and may not work correctly. 23 | 24 | Prerequisites 25 | ------------- 26 | * JDK 17.x 27 | * Maven 3.x 28 | -------------------------------------------------------------------------------- /src/main/java/com/ofallonfamily/jersey2akka/ExampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.ofallonfamily.jersey2akka; 2 | 3 | import akka.actor.ActorSystem; 4 | import akka.routing.RoundRobinPool; 5 | import org.glassfish.jersey.internal.inject.AbstractBinder; 6 | import org.glassfish.jersey.server.ResourceConfig; 7 | 8 | import jakarta.annotation.PreDestroy; 9 | import jakarta.ws.rs.ApplicationPath; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | @ApplicationPath("examples") 13 | public class ExampleApplication extends ResourceConfig { 14 | 15 | private ActorSystem system; 16 | 17 | public ExampleApplication() { 18 | 19 | system = ActorSystem.create("ExampleSystem"); 20 | system.actorOf(DoublingActor.mkProps().withRouter(new RoundRobinPool(5)), "doublingRouter"); 21 | 22 | register(new AbstractBinder() { 23 | protected void configure() { 24 | bind(system).to(ActorSystem.class); 25 | } 26 | }); 27 | 28 | packages("com.ofallonfamily.jersey2akka"); 29 | 30 | } 31 | 32 | @PreDestroy 33 | private void shutdown() { 34 | system.terminate(); 35 | try { 36 | system.getWhenTerminated().toCompletableFuture().get(15, TimeUnit.SECONDS); 37 | } catch (Exception e) { 38 | // Ignore timeout exceptions 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/main/java/com/ofallonfamily/jersey2akka/ExampleService.java: -------------------------------------------------------------------------------- 1 | package com.ofallonfamily.jersey2akka; 2 | 3 | import akka.actor.ActorSelection; 4 | import akka.actor.ActorSystem; 5 | import akka.dispatch.OnComplete; 6 | import akka.event.LoggingAdapter; 7 | import akka.pattern.Patterns; 8 | import akka.util.Timeout; 9 | import org.glassfish.jersey.server.ManagedAsync; 10 | import scala.concurrent.Future; 11 | import scala.concurrent.duration.Duration; 12 | 13 | import jakarta.ws.rs.GET; 14 | import jakarta.ws.rs.Path; 15 | import jakarta.ws.rs.PathParam; 16 | import jakarta.ws.rs.Produces; 17 | import jakarta.ws.rs.container.AsyncResponse; 18 | import jakarta.ws.rs.container.Suspended; 19 | import jakarta.ws.rs.core.Context; 20 | import jakarta.ws.rs.core.MediaType; 21 | import jakarta.ws.rs.core.Response; 22 | import java.util.HashMap; 23 | 24 | @Path("/doubler/{value}") 25 | public class ExampleService { 26 | 27 | @Context ActorSystem actorSystem; 28 | LoggingAdapter log; 29 | 30 | @GET 31 | @Produces(MediaType.APPLICATION_JSON) 32 | @ManagedAsync 33 | public void getExamples ( 34 | @PathParam("value") Integer value, 35 | @Suspended final AsyncResponse res) { 36 | 37 | ActorSelection doublingActor = actorSystem.actorSelection("/user/doublingRouter"); 38 | 39 | Timeout timeout = new Timeout(Duration.create(2, "seconds")); 40 | 41 | Future future = Patterns.ask(doublingActor, value, timeout); 42 | 43 | future.onComplete(new OnComplete() { 44 | 45 | public void onComplete(Throwable failure, Object result) { 46 | 47 | if (failure != null) { 48 | 49 | if (failure.getMessage() != null) { 50 | HashMap response = new HashMap(); 51 | response.put("error", failure.getMessage()); 52 | res.resume(Response.serverError().entity(response).build()); 53 | } else { 54 | res.resume(Response.serverError()); 55 | } 56 | 57 | } else { 58 | 59 | HashMap response = new HashMap(); 60 | response.put("results",result); 61 | res.resume(Response.ok().entity(response).build()); 62 | 63 | } 64 | 65 | } 66 | }, actorSystem.dispatcher()); 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.ofallonfamily.examples 5 | jersey2-akka-java 6 | war 7 | 1.0-SNAPSHOT 8 | jersey2-akka-java 9 | 10 | 11 | 12 | 13 | org.glassfish.jersey 14 | jersey-bom 15 | 3.1.11 16 | pom 17 | import 18 | 19 | 20 | 21 | 22 | 23 | 24 | com.typesafe.akka 25 | akka-actor_2.13 26 | 2.8.8 27 | 28 | 29 | org.glassfish.jersey.containers 30 | jersey-container-servlet 31 | 32 | 33 | org.glassfish.jersey.inject 34 | jersey-hk2 35 | 36 | 37 | org.glassfish.jersey.media 38 | jersey-media-json-jackson 39 | 40 | 41 | org.glassfish.jersey.test-framework.providers 42 | jersey-test-framework-provider-jetty 43 | test 44 | 45 | 46 | org.glassfish.jersey.connectors 47 | jersey-apache-connector 48 | test 49 | 50 | 51 | junit 52 | junit 53 | 4.13.2 54 | test 55 | 56 | 57 | org.junit.vintage 58 | junit-vintage-engine 59 | 5.13.4 60 | test 61 | 62 | 63 | 64 | 65 | 66 | 67 | src/main/resources 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-compiler-plugin 74 | 3.14.0 75 | 76 | 17 77 | 17 78 | UTF-8 79 | 80 | 81 | 82 | org.apache.maven.plugins 83 | maven-surefire-plugin 84 | 3.5.3 85 | 86 | false 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-war-plugin 95 | 3.4.0 96 | 97 | false 98 | 99 | 100 | 101 | org.apache.tomcat.maven 102 | tomcat7-maven-plugin 103 | 2.2 104 | 105 | 9090 106 | / 107 | false 108 | 109 | 110 | 111 | org.codehaus.cargo 112 | cargo-maven3-plugin 113 | 1.10.21 114 | 115 | 116 | tomcat10x 117 | embedded 118 | 119 | org.apache.tomcat 120 | tomcat 121 | 10.1.43 122 | tar.gz 123 | 124 | 125 | 126 | 127 | 9090 128 | 129 | 130 | 131 | 132 | ${project.groupId} 133 | ${project.artifactId} 134 | war 135 | 136 | / 137 | 138 | 139 | 140 | 141 | 142 | 143 | org.jacoco 144 | jacoco-maven-plugin 145 | 0.8.13 146 | 147 | 148 | 149 | prepare-agent 150 | 151 | 152 | 153 | report 154 | test 155 | 156 | report 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Jersey2-Akka-Java Development Instructions 2 | 3 | Jersey2-Akka-Java is an example asynchronous REST API written in Java using Jersey 2.23 and Akka 2.3.15. This project demonstrates instantiating an Akka ActorSystem at server startup and using it to fulfill asynchronous Jersey REST service invocations. 4 | 5 | **Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.** 6 | 7 | ## Working Effectively 8 | 9 | ### Prerequisites and Setup 10 | - JDK 17+ is installed and working (despite README stating JDK 7.x requirement) 11 | - Maven 3.x is available and working 12 | - No additional setup required - Maven will download all dependencies automatically 13 | 14 | ### Build Commands (NEVER CANCEL - Use Long Timeouts) 15 | - **Compile only**: `mvn clean compile` -- takes 20 seconds. NEVER CANCEL. Set timeout to 60+ seconds on first run due to dependency downloads. 16 | - **Run tests**: `mvn test` -- takes 10 seconds. NEVER CANCEL. Set timeout to 30+ seconds. 17 | - **WARNING**: `mvn clean package` and `mvn clean install` FAIL due to Maven War Plugin incompatibility with JDK 17. Do NOT attempt these commands. 18 | 19 | ### Development Workflow 20 | 1. **Always start by compiling**: `mvn clean compile` 21 | 2. **Run tests to verify changes**: `mvn test` 22 | 3. **Start the application**: `mvn tomcat7:run` 23 | 4. **Test functionality manually** using the validation scenarios below 24 | 25 | ### Running the Application 26 | - **Start server**: `mvn tomcat7:run` -- starts Tomcat on port 9090. NEVER CANCEL. Set timeout to 60+ seconds. 27 | - **Base URL**: http://localhost:9090/ 28 | - **API endpoint**: http://localhost:9090/examples/doubler/{value} 29 | - **Stop server**: Use Ctrl+C or stop the Maven process 30 | 31 | ## Validation Scenarios 32 | 33 | **ALWAYS test these complete end-to-end scenarios after making changes:** 34 | 35 | ### Basic Functionality Test 36 | ```bash 37 | # Start the application first 38 | mvn tomcat7:run 39 | 40 | # In another terminal, test the API: 41 | curl http://localhost:9090/examples/doubler/2 42 | # Expected output: {"results" : 4} 43 | 44 | curl http://localhost:9090/examples/doubler/5 45 | # Expected output: {"results" : 10} 46 | 47 | curl http://localhost:9090/examples/doubler/10 48 | # Expected output: {"results" : 20} 49 | ``` 50 | 51 | ### Complete Development Validation 52 | 1. **Build and test**: `mvn clean compile && mvn test` 53 | 2. **Start application**: `mvn tomcat7:run` 54 | 3. **Verify API responses** using curl commands above 55 | 4. **Check logs** for any ERROR messages (warnings are expected) 56 | 5. **Stop application** when testing complete 57 | 58 | ## Common Issues and Limitations 59 | 60 | ### Known Compatibility Issues 61 | - **WAR packaging fails**: The Maven War Plugin 2.6 is incompatible with JDK 17. Commands like `mvn clean package` and `mvn clean install` will fail with "ExceptionInInitializerError" related to TreeMap comparator access. 62 | - **Solution**: Use `mvn tomcat7:run` for development and testing. The application runs correctly despite packaging issues. 63 | 64 | ### Expected Warnings 65 | - Jersey validation warnings during startup are normal and do not affect functionality 66 | - Platform encoding warnings during compilation are expected 67 | - Deprecated API warnings in test compilation are expected 68 | 69 | ## Important Notes 70 | 71 | ### Timeout Requirements 72 | - **Compilation**: NEVER CANCEL before 60 seconds (20s typical + dependency downloads) 73 | - **Tests**: NEVER CANCEL before 30 seconds (10s typical) 74 | - **Application startup**: NEVER CANCEL before 60 seconds (15s typical + startup time) 75 | 76 | ### Code Structure 77 | - **Main application**: `src/main/java/com/ofallonfamily/jersey2akka/ExampleApplication.java` 78 | - **REST service**: `src/main/java/com/ofallonfamily/jersey2akka/ExampleService.java` 79 | - **Actor implementation**: `src/main/java/com/ofallonfamily/jersey2akka/DoublingActor.java` 80 | - **Test**: `src/test/java/com/ofallonfamily/jersey2akka/DoublerTest.java` 81 | - **Configuration**: `src/main/resources/application.conf` 82 | 83 | ### Development Best Practices 84 | - Always run `mvn clean compile` before testing changes 85 | - Always run `mvn test` to verify unit tests pass 86 | - Always manually test the REST API after code changes 87 | - Use the tomcat7 plugin for local development - it works reliably 88 | - Do not attempt WAR packaging unless you are prepared to handle Maven plugin compatibility issues 89 | 90 | ## Project Overview 91 | 92 | ### Key Components 93 | - **Jersey 2.23**: JAX-RS implementation for REST services 94 | - **Akka 2.3.15**: Actor framework for asynchronous processing 95 | - **Round-robin router**: Distributes work across 5 DoublingActor instances 96 | - **Jackson JSON**: Handles JSON serialization/deserialization 97 | - **Tomcat 7 Maven Plugin**: Provides embedded Tomcat for development 98 | 99 | ### Architecture 100 | 1. `ExampleApplication` extends `ResourceConfig` and sets up Akka ActorSystem 101 | 2. `ExampleService` provides REST endpoint that sends messages to Akka actors 102 | 3. `DoublingActor` processes integer doubling requests asynchronously 103 | 4. Jersey test framework provides integration testing capabilities 104 | 105 | This is a learning/example project demonstrating Jersey + Akka integration patterns. 106 | 107 | ## Quick Reference 108 | 109 | ### Repository Structure 110 | ``` 111 | . 112 | ├── .github/ 113 | │ └── copilot-instructions.md # This file 114 | ├── .gitignore 115 | ├── .travis.yml # CI configuration 116 | ├── README.md # Basic project information 117 | ├── pom.xml # Maven configuration 118 | └── src/ 119 | ├── main/ 120 | │ ├── java/com/ofallonfamily/jersey2akka/ 121 | │ │ ├── ExampleApplication.java # Main application entry point 122 | │ │ ├── ExampleService.java # REST endpoint implementation 123 | │ │ └── DoublingActor.java # Akka actor for processing 124 | │ └── resources/ 125 | │ └── application.conf # Akka configuration 126 | └── test/ 127 | └── java/com/ofallonfamily/jersey2akka/ 128 | └── DoublerTest.java # Integration test 129 | ``` 130 | 131 | ### Expected Command Output Examples 132 | 133 | #### Successful Compile 134 | ``` 135 | [INFO] BUILD SUCCESS 136 | [INFO] Total time: 1.373 s 137 | ``` 138 | 139 | #### Successful Test 140 | ``` 141 | [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 142 | [INFO] BUILD SUCCESS 143 | ``` 144 | 145 | #### Application Startup (Final Line) 146 | ``` 147 | INFO: Starting ProtocolHandler ["http-bio-9090"] 148 | ``` 149 | 150 | #### API Response Examples 151 | ```bash 152 | $ curl http://localhost:9090/examples/doubler/2 153 | { 154 | "results" : 4 155 | } 156 | ``` --------------------------------------------------------------------------------