From 56ea58b44fb9deecc80b44f593c45e233de2ff74 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 27 May 2025 07:56:14 +1200 Subject: [PATCH 1/3] Add avaje-nima page --- _layout/base-nima.html | 32 +++++++++++++++++ _nima/_nav.ftl | 6 ++++ nima/index.html | 80 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 _layout/base-nima.html create mode 100644 _nima/_nav.ftl create mode 100644 nima/index.html diff --git a/_layout/base-nima.html b/_layout/base-nima.html new file mode 100644 index 0000000..785aa55 --- /dev/null +++ b/_layout/base-nima.html @@ -0,0 +1,32 @@ + + + + + + + https://github.com/avaje/avaje-nima + + +
+ +
+
+ +
+
+
+
+ + diff --git a/_nima/_nav.ftl b/_nima/_nav.ftl new file mode 100644 index 0000000..8c70825 --- /dev/null +++ b/_nima/_nav.ftl @@ -0,0 +1,6 @@ + +

 

diff --git a/nima/index.html b/nima/index.html new file mode 100644 index 0000000..ea55f85 --- /dev/null +++ b/nima/index.html @@ -0,0 +1,80 @@ + + + + + <#assign index="active"> + + + +

+  Nima +

+ + + + + + + + + + + + + + + + +
DiscordSourceAPI DocsIssuesReleases
DiscordGitHubJavadoc + GitHub
+ +



+ A combination of Helidon SE Webserver and Avaje libraries, includes: +

+ + + +

Maven dependencies

+

1. Add avaje-nima dependencies

+
+    
+      io.avaje
+      avaje-nima
+      ${avaje.nima.version}
+    
+
+    
+    
+      io.avaje
+      avaje-nima-test
+      ${avaje.nima.version}
+      test
+    
+  
+ +

2. Add the annotation processor

+
+    
+    
+      io.avaje
+      avaje-nima-generator
+      ${avaje.nima.version}
+      provided
+      true
+    
+  
+ + +







+ + + From 9e7c5c9f91d2dad61f44e336406312f132e5a01c Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 17 Jun 2025 08:11:26 +1200 Subject: [PATCH 2/3] Add avaje-nima page --- jsonb/index.html | 52 +++---- nima/index.html | 373 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 391 insertions(+), 34 deletions(-) diff --git a/jsonb/index.html b/jsonb/index.html index 5782323..114939b 100644 --- a/jsonb/index.html +++ b/jsonb/index.html @@ -70,32 +70,32 @@

1. Add avaje-jsonb dependencies.

-
-  io.avaje
-  avaje-jsonb
-  ${avaje.jsonb.version}
-
-
-
-  io.avaje
-  avaje-jsonb-spring-starter
-  ${avaje.jsonb.version}
-
-
+ + io.avaje + avaje-jsonb + ${avaje.jsonb.version} + + + + io.avaje + avaje-jsonb-spring-starter + ${avaje.jsonb.version} + +

2. Add the annotation processor to your pom.

-  
-  
-    io.avaje
-    avaje-jsonb-generator
-    ${avaje.jsonb.version}
-    provided
-    true
-  
-
+ + + io.avaje + avaje-jsonb-generator + ${avaje.jsonb.version} + provided + true + +

2a. JDK 23+

@@ -142,10 +142,10 @@

// build using defaults Jsonb jsonb = Jsonb.builder().build(); - JsonType customerType = jsonb.type(Customer.class); + JsonType<|Customer> customerType = jsonb.type(Customer.class); // If the type is generic we can specify - // JsonType> customerType = jsonb.type(Types.newParameterizedType(Customer.class, T1.class,T2.class, ...); + // JsonType<|Customer<|T1,T2,...>> customerType = jsonb.type(Types.newParameterizedType(Customer.class, T1.class,T2.class, ...); Customer customer = ...; @@ -165,14 +165,14 @@

   Jsonb jsonb = Jsonb.builder().build();
 
-  JsonType customerType = jsonb.type(Customer.class);
+  JsonType<|Customer> customerType = jsonb.type(Customer.class);
 
   // only including the id and name
-  JsonView idAndNameView = customerType.view("(id, name)");
+  JsonView<|Customer> idAndNameView = customerType.view("(id, name)");
 
   String asJson = idAndNameView.toJson(customer);
 
-  JsonView myView =
+  JsonView<|Customer> myView =
     customerType.view("(id, name, billingAddress(*), contacts(lastName, email))");
 
   // serialize to json the above specified properties only
diff --git a/nima/index.html b/nima/index.html
index ea55f85..1e3776c 100644
--- a/nima/index.html
+++ b/nima/index.html
@@ -63,14 +63,371 @@ 

1. Add avaje-nima dependencies

2. Add the annotation processor

-    
-    
-      io.avaje
-      avaje-nima-generator
-      ${avaje.nima.version}
-      provided
-      true
-    
+    
+      
+        
+          org.apache.maven.plugins
+          maven-compiler-plugin
+          3.14.0
+          
+            
+            
+              
+                io.avaje
+                avaje-nima-generator
+                ${avaje-nima.version}
+              
+            
+          
+        
+      
+    
+  
+

+ Note the avaje-nima-generator is actually a composite of: +

+
    +
  • avaje-http-helidon-generator - for the route adapter
  • +
  • avaje-inject-generator - for dependency injection
  • +
  • avaje-jsonb-generator - for json adapters
  • +
  • avaje-validator-generator - for bean validation
  • +
  • avaje-record-builder - for record builders
  • +
  • avaje-spi-service - for automatic service generation
  • +
  • jstachio-apt - for mustache template rendering
  • +
  • avaje-http-client-generator - for test client generation
  • +
+ + +

Controller

+

+ Create a package (e.g. org.example.web) and create you first + controller in the package. +

+ +
+    package org.example.web;
+
+    import io.avaje.http.api.*;
+    import io.avaje.inject.*;
+    import io.avaje.jsonb.Json;
+
+    @Controller
+    public class HelloController {
+
+      @Produces("text/plain")
+      @Get("/")
+      String hello() {
+        return "hello world";
+      }
+
+      @Get("/json")
+      HelloBean one() {
+        return new HelloBean(97, "Hello JSON");
+      }
+
+      @Json
+      public record HelloBean(int id, String name) {
+
+      }
+    }
+  
+ +

Generated code

+

+ After compiling HelloController we should see in target/generated-sources/annotations: +

+
    +
  • HelloController$DI - which performs the dependency injection wiring of the controller
  • +
  • HelloController$Route - which registers the controller routes with Helidon and adapts the + Helidon request and response to our controller code
  • +
  • HelloController$Route$DI - the dependency injection for the routes
  • +
  • HelloController$HelloBeanJsonAdapter - the JSON adapter for HelloBean
  • +
+ +

Generated test code

+

+ And for test purposes in target/generated-test-sources/test-annotations + a couple of things are generated for us: +

+
    +
  • HelloControllerTestAPI - a test client we can use to test the controller
  • +
  • HelloControllerTestAPIHttpClient - the test client implementation
  • +
+ +

Test client - HelloControllerTestAPI

+

+ We can use the generated test client to create a component test, and test our controller. + The name of the generated test clients is: {controllerName} + TestAPI. +

+ +

Controller component test

+

+ Create a test class called HelloControllerTest to test the controller. +

+ +
+    package org.example.web;
+
+    import io.avaje.inject.test.InjectTest;
+    import jakarta.inject.Inject;
+    import org.junit.jupiter.api.Test;
+
+    import java.net.http.HttpResponse;
+
+    import static org.assertj.core.api.Assertions.assertThat;
+
+    @InjectTest
+    class HelloControllerTest {
+
+      @Inject HelloControllerTestAPI client;
+
+      @Test
+      void hello() {
+
+        HttpResponse<|String> res = client.hello();
+        assertThat(res.statusCode()).isEqualTo(200);
+        assertThat(res.body()).isEqualTo("hello world");
+      }
+    }
+  
+
    +
  • Use @InjectTest for a DI component test which can wire dependencies
  • +
  • Use @Inject HelloControllerTestAPI client to wire the test http client. The webserver will + start on a random port and be available for this test.
  • +
+ +

Main method

+

+ Create a main method to run the application. +

+
+    package org.example;
+
+    import io.avaje.nima.Nima;
+
+    public class Main {
+
+      public static void main(String[] args) {
+
+        var webServer = Nima.builder()
+          .port(8080)
+          .build();
+
+        webServer.start();
+      }
+    }
+  
+

+ Now you can run the application main method, +

+
+21:23:37.951 [main] INFO  io.avaje.inject - Wired beans in 77ms
+21:23:38.001 [features-thread] INFO  i.h.common.features.HelidonFeatures - Helidon SE 4.2.2 features: [Config, Encoding, Media, Metrics, Registry, WebServer]
+21:23:38.005 [start @default (/0.0.0.0:8080)] INFO  io.helidon.webserver.ServerListener - [0x2132b530] http://0.0.0.0:8080 bound for socket '@default'
+21:23:38.008 [main] INFO  io.helidon.webserver.LoomServer - Started all channels in 6 milliseconds. 369 milliseconds since JVM startup. Java 21.0.5+9-LTS-239
+  
+

+ ... and perform a quick test using curl. +

+
+    curl http://localhost:8080
+    hello world
+
+    curl http://localhost:8080/json
+    {"id":97,"name":"Hello JSON"}
+  
+ +

Error handlers

+

+ Commonly we want define all the global exception handling in a single exception handler. + We can do this by creating a controller than just has @ExceptionHandler methods. +

+

+ An example ErrorHandlers controller is: +

+
+    package org.example.web;
+
+    import io.avaje.http.api.Controller;
+    import io.avaje.http.api.ExceptionHandler;
+    import io.avaje.http.api.Produces;
+    import io.helidon.http.BadRequestException;
+    import io.helidon.webserver.http.ServerRequest;
+    import org.slf4j.Logger;
+    import org.slf4j.LoggerFactory;
+
+    import java.util.UUID;
+
+    @Controller
+    final class ErrorHandlers {
+
+      private static final Logger log = LoggerFactory.getLogger(ErrorHandlers.class);
+
+      @Produces(statusCode = 500)
+      @ExceptionHandler
+      ErrorResponse defaultError(Exception ex, ServerRequest req) {
+        var path = path(req);
+        var traceId = log(ex, path);
+        return ErrorResponse.builder()
+          .statusCode(500)
+          .path(path)
+          .traceId(traceId)
+          .message("Unhandled server error")
+          .build();
+      }
+
+      @Produces(statusCode = 400)
+      @ExceptionHandler
+      ErrorResponse badRequest(BadRequestException ex, ServerRequest req) {
+        var path = path(req);
+        var traceId = log(ex, path);
+        return ErrorResponse.builder()
+          .statusCode(400)
+          .path(path)
+          .traceId(traceId)
+          .message("Unhandled server error")
+          .build();
+      }
+
+      private static String path(ServerRequest req) {
+        return req != null && req.path() != null ? req.path().path() : null;
+      }
+
+      private static UUID log(Throwable ex, String path) {
+        UUID traceId = UUID.randomUUID();
+        log.error("Unhandled server error path:{} trace:{}", path, traceId, ex);
+        return traceId;
+      }
+
+    }
+  
+

+ With the above example the error response has a JSON payload. The code for the + ErrorResponse is: +

+
+    import io.avaje.jsonb.Json;
+    import io.avaje.recordbuilder.RecordBuilder;
+
+    import java.util.UUID;
+
+    @Json
+    @RecordBuilder
+    public record ErrorResponse(
+
+      int statusCode,
+      String path,
+      UUID traceId,
+      String message) {
+
+      public static ErrorResponseBuilder builder() {
+        return ErrorResponseBuilder.builder();
+      }
+    }
+  
+
+ Generated Code: (click to expand) +
+      @Generated("avaje-helidon-generator")
+      @Component
+      public final class ErrorHandlers$Route implements HttpFeature {
+
+        private final ErrorHandlers controller;
+        private final JsonType<|ErrorResponse> errorResponseJsonType;
+
+        public ErrorHandlers$Route(ErrorHandlers controller, Jsonb jsonb) {
+          this.controller = controller;
+          this.errorResponseJsonType = jsonb.type(ErrorResponse.class);
+        }
+
+        @Override
+        public void setup(HttpRouting.Builder routing) {
+          routing.error(Exception.class, this::_defaultError);
+          routing.error(BadRequestException.class, this::_badRequest);
+        }
+
+        private void _defaultError(ServerRequest req, ServerResponse res, Exception ex) {
+          res.status(INTERNAL_SERVER_ERROR_500);
+          var result = controller.defaultError(ex, req);
+          if (result == null) {
+            res.status(NO_CONTENT_204).send();
+          } else {
+            res.headers().contentType(MediaTypes.APPLICATION_JSON);
+            errorResponseJsonType.toJson(result, JsonOutput.of(res));
+          }
+        }
+
+        private void _badRequest(ServerRequest req, ServerResponse res, BadRequestException ex) {
+          res.status(BAD_REQUEST_400);
+          var result = controller.badRequest(ex, req);
+          if (result == null) {
+            res.status(NO_CONTENT_204).send();
+          } else {
+            res.headers().contentType(MediaTypes.APPLICATION_JSON);
+            errorResponseJsonType.toJson(result, JsonOutput.of(res));
+          }
+        }
+
+      }
+    
+
+ +

Filters

+

+ To add a Filter we can add a controller that has a @Filter method + that takes a FilterChain, RoutingRequest and + optionally a RoutingResponse. +

+

+ An example filter that reads a "Caller-Id" request header and rejects the + request if one isn't provided. +

+
+    import io.avaje.http.api.Controller;
+    import io.avaje.http.api.Filter;
+    import io.helidon.http.BadRequestException;
+    import io.helidon.http.HeaderName;
+    import io.helidon.http.HeaderNames;
+    import io.helidon.webserver.http.FilterChain;
+    import io.helidon.webserver.http.RoutingRequest;
+    import io.helidon.webserver.http.ServerRequest;
+
+    import java.util.Optional;
+    import java.util.Set;
+
+    @Controller
+    final class CallerIdFilter {
+
+      private static final HeaderName CALLER_ID = HeaderNames.create("Caller-Id");
+
+      private static final Set BYPASS = Set.of("/ping");
+
+      @Filter
+      void filter(FilterChain chain, RoutingRequest req) {
+        var path = path(req);
+        if (BYPASS.contains(path)) {
+          chain.proceed();
+        } else {
+          String callerId = callerId(req).orElseThrow(() -> new BadRequestException("Caller-Id required"));
+          handleCallerMetrics(path, callerId);
+          chain.proceed();
+        }
+      }
+
+      private void handleCallerMetrics(String path, String callerId) {
+        // capture metrics
+      }
+
+      private Optional callerId(RoutingRequest request) {
+        return request.headers().first(CALLER_ID);
+      }
+
+
+      private static String path(ServerRequest req) {
+        return req != null && req.path() != null ? req.path().path() : null;
+      }
+
+    }
   
From 500ed3e37c0cde45a25450f060ab50ec20e4bf38 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:34:47 -0400 Subject: [PATCH 3/3] change wording and ordering --- nima/index.html | 178 +++++++++++++++++++++++------------------------- 1 file changed, 86 insertions(+), 92 deletions(-) diff --git a/nima/index.html b/nima/index.html index 1e3776c..316a8b0 100644 --- a/nima/index.html +++ b/nima/index.html @@ -1,7 +1,8 @@ + - + <#assign index="active"> @@ -34,15 +35,14 @@

A combination of Helidon SE Webserver and Avaje libraries, includes:

    -
  • Helidon 4 SE WebServer (Uses Virtual Threads)
  • -
  • avaje-inject - Dependency injection using source code generation via annotation processing
  • -
  • avaje-http - Controllers generated as source code using annotation processing
  • -
  • avaje-jsonb - JSON adapters generated as source code using annotation processing
  • -
  • avaje-validator - JSR validation generated as source code using annotation processing
  • -
  • avaje-config - External configuration
  • +
  • Helidon SE - High performance webserver
  • +
  • avaje-inject - Dependency injection
  • +
  • avaje-http - JAX-RS style controller generation
  • +
  • avaje-jsonb - JSON adapter generation
  • +
  • avaje-validator - Bean validation
  • +
  • avaje-config - External configuration
-

Maven dependencies

1. Add avaje-nima dependencies

@@ -62,29 +62,8 @@ 

1. Add avaje-nima dependencies

2. Add the annotation processor

-
-    
-      
-        
-          org.apache.maven.plugins
-          maven-compiler-plugin
-          3.14.0
-          
-            
-            
-              
-                io.avaje
-                avaje-nima-generator
-                ${avaje-nima.version}
-              
-            
-          
-        
-      
-    
-  

- Note the avaje-nima-generator is actually a composite of: + avaje-nima-generator is a composite of:

  • avaje-http-helidon-generator - for the route adapter
  • @@ -96,12 +75,30 @@

    2. Add the annotation processor

  • jstachio-apt - for mustache template rendering
  • avaje-http-client-generator - for test client generation
+
+  
+  
+    io.avaje
+    avaje-nima-generator
+    ${avaje.nima.version}
+    provided
+    true
+  
+  
+

2a. JDK 23+

+ +

In JDK 23+, annotation processors are disabled by default, so we need to add a compiler property to re-enable.

+ +
+  
+    full
+  
+  

Controller

- Create a package (e.g. org.example.web) and create you first - controller in the package. + Create a controller and annotate it with http-api annotations.

@@ -127,42 +124,81 @@ 

Controller

@Json public record HelloBean(int id, String name) { + } + } +
+ + +

Nima Class

+

+ The Nima class will start a BeanScope, register generated controller routes, and start the helidon webserver. +

+ The Nima class will search your BeanScope for a WebServerConfig.Builder, if you provide one in your + BeanScope it will be used to configure the webserver. +

+
+    package org.example;
+
+    import io.avaje.nima.Nima;
 
+    public class Main {
+
+      public static void main(String[] args) {
+
+        var webServer = Nima.builder()
+          .port(8080)
+          .build();
+
+        webServer.start();
       }
     }
   
+

+ Now you can run the application main method, +

+
+21:23:37.951 [main] INFO  io.avaje.inject - Wired beans in 77ms
+21:23:38.001 [features-thread] INFO  i.h.common.features.HelidonFeatures - Helidon SE 4.2.2 features: [Config, Encoding, Media, Metrics, Registry, WebServer]
+21:23:38.005 [start @default (/0.0.0.0:8080)] INFO  io.helidon.webserver.ServerListener - [0x2132b530] http://0.0.0.0:8080 bound for socket '@default'
+21:23:38.008 [main] INFO  io.helidon.webserver.LoomServer - Started all channels in 6 milliseconds. 369 milliseconds since JVM startup. Java 21.0.5+9-LTS-239
+  
+

+ ... and perform a quick test using curl. +

+
+    curl http://localhost:8080
+    hello world
+
+    curl http://localhost:8080/json
+    {"id":97,"name":"Hello JSON"}
+  

Generated code

- After compiling HelloController we should see in target/generated-sources/annotations: + After compiling, we should see in target/generated-sources/annotations:

  • HelloController$DI - which performs the dependency injection wiring of the controller
  • HelloController$Route - which registers the controller routes with Helidon and adapts the - Helidon request and response to our controller code
  • + Helidon request and response to the controller code
  • HelloController$Route$DI - the dependency injection for the routes
  • HelloController$HelloBeanJsonAdapter - the JSON adapter for HelloBean

Generated test code

- And for test purposes in target/generated-test-sources/test-annotations - a couple of things are generated for us: + In target/generated-test-sources/test-annotations + useful testing classes are generated for us:

    -
  • HelloControllerTestAPI - a test client we can use to test the controller
  • +
  • HelloControllerTestAPI - a test client inteface to test the controller
  • HelloControllerTestAPIHttpClient - the test client implementation
-

Test client - HelloControllerTestAPI

-

- We can use the generated test client to create a component test, and test our controller. - The name of the generated test clients is: {controllerName} + TestAPI. -

-

Controller component test

- Create a test class called HelloControllerTest to test the controller. + Use the generated test client to create a component test that tests our controller. + The name of the generated test clients is: {controllerName} + TestAPI.

@@ -191,59 +227,18 @@ 

Controller component test

}
    -
  • Use @InjectTest for a DI component test which can wire dependencies
  • +
  • Use @InjectTest to allow the test which to wire dependencies
  • Use @Inject HelloControllerTestAPI client to wire the test http client. The webserver will - start on a random port and be available for this test.
  • + start on a random port and be available for this test.
-

Main method

-

- Create a main method to run the application. -

-
-    package org.example;
-
-    import io.avaje.nima.Nima;
-
-    public class Main {
-
-      public static void main(String[] args) {
-
-        var webServer = Nima.builder()
-          .port(8080)
-          .build();
-
-        webServer.start();
-      }
-    }
-  
-

- Now you can run the application main method, -

-
-21:23:37.951 [main] INFO  io.avaje.inject - Wired beans in 77ms
-21:23:38.001 [features-thread] INFO  i.h.common.features.HelidonFeatures - Helidon SE 4.2.2 features: [Config, Encoding, Media, Metrics, Registry, WebServer]
-21:23:38.005 [start @default (/0.0.0.0:8080)] INFO  io.helidon.webserver.ServerListener - [0x2132b530] http://0.0.0.0:8080 bound for socket '@default'
-21:23:38.008 [main] INFO  io.helidon.webserver.LoomServer - Started all channels in 6 milliseconds. 369 milliseconds since JVM startup. Java 21.0.5+9-LTS-239
-  
-

- ... and perform a quick test using curl. -

-
-    curl http://localhost:8080
-    hello world
-
-    curl http://localhost:8080/json
-    {"id":97,"name":"Hello JSON"}
-  
-

Error handlers

- Commonly we want define all the global exception handling in a single exception handler. - We can do this by creating a controller than just has @ExceptionHandler methods. + Commonly exception handling is done in a dedicated exception handling class. + This can be done by creating a controller that has @ExceptionHandler methods.

- An example ErrorHandlers controller is: + Example error controller:

     package org.example.web;
@@ -314,7 +309,6 @@ 

Error handlers

@Json @RecordBuilder public record ErrorResponse( - int statusCode, String path, UUID traceId, @@ -379,7 +373,7 @@

Filters

optionally a RoutingResponse.

- An example filter that reads a "Caller-Id" request header and rejects the + Example filter that reads a "Caller-Id" request header and rejects the request if one isn't provided.