From befcb7f78a6d689f71f2e57a401337acd493babb Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 14 Jan 2026 11:52:17 -0300 Subject: [PATCH 01/31] prepare for next development cycle --- jooby/pom.xml | 2 +- modules/jooby-apt/pom.xml | 2 +- modules/jooby-avaje-inject/pom.xml | 2 +- modules/jooby-avaje-jsonb/pom.xml | 2 +- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v1/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-bom/pom.xml | 4 ++-- modules/jooby-caffeine/pom.xml | 2 +- modules/jooby-camel/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-commons-email/pom.xml | 2 +- modules/jooby-conscrypt/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-distribution/pom.xml | 2 +- modules/jooby-ebean/pom.xml | 2 +- modules/jooby-flyway/pom.xml | 2 +- modules/jooby-freemarker/pom.xml | 2 +- modules/jooby-gradle-setup/pom.xml | 2 +- modules/jooby-graphiql/pom.xml | 2 +- modules/jooby-graphql/pom.xml | 2 +- modules/jooby-gson/pom.xml | 2 +- modules/jooby-guice/pom.xml | 2 +- modules/jooby-handlebars/pom.xml | 2 +- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-hibernate/pom.xml | 2 +- modules/jooby-hikari/pom.xml | 2 +- modules/jooby-jackson/pom.xml | 2 +- modules/jooby-jasypt/pom.xml | 2 +- modules/jooby-jdbi/pom.xml | 2 +- modules/jooby-jetty/pom.xml | 2 +- modules/jooby-jstachio/pom.xml | 2 +- modules/jooby-jte/pom.xml | 2 +- modules/jooby-jwt/pom.xml | 2 +- modules/jooby-kafka/pom.xml | 2 +- modules/jooby-kotlin/pom.xml | 2 +- modules/jooby-log4j/pom.xml | 2 +- modules/jooby-logback/pom.xml | 2 +- modules/jooby-maven-plugin/pom.xml | 2 +- modules/jooby-metrics/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-netty/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-pac4j/pom.xml | 2 +- modules/jooby-pebble/pom.xml | 2 +- modules/jooby-quartz/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-redis/pom.xml | 2 +- modules/jooby-redoc/pom.xml | 2 +- modules/jooby-rocker/pom.xml | 2 +- modules/jooby-run/pom.xml | 2 +- modules/jooby-rxjava3/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- modules/jooby-swagger-ui/pom.xml | 2 +- modules/jooby-test/pom.xml | 2 +- modules/jooby-thymeleaf/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- modules/jooby-vertx-mysql-client/pom.xml | 2 +- modules/jooby-vertx-pg-client/pom.xml | 2 +- modules/jooby-vertx-sql-client/pom.xml | 2 +- modules/jooby-vertx/pom.xml | 2 +- modules/jooby-whoops/pom.xml | 2 +- modules/jooby-yasson/pom.xml | 2 +- modules/pom.xml | 2 +- pom.xml | 4 ++-- tests/pom.xml | 2 +- 66 files changed, 68 insertions(+), 68 deletions(-) diff --git a/jooby/pom.xml b/jooby/pom.xml index d3fb5db11e..659b29a13f 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.15 + 4.0.16-SNAPSHOT jooby jooby diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index b94e7f7603..cb94b192dd 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-apt jooby-apt diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml index 0b7dc1190d..c0f2e8243c 100644 --- a/modules/jooby-avaje-inject/pom.xml +++ b/modules/jooby-avaje-inject/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-avaje-inject jooby-avaje-inject diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index 7dff5e6c75..1c0fe29d63 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-avaje-jsonb jooby-avaje-jsonb diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index c74d132ca8..971b651aad 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-avaje-validator jooby-avaje-validator diff --git a/modules/jooby-awssdk-v1/pom.xml b/modules/jooby-awssdk-v1/pom.xml index d509fcec98..2fa34972ad 100644 --- a/modules/jooby-awssdk-v1/pom.xml +++ b/modules/jooby-awssdk-v1/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-awssdk-v1 jooby-awssdk-v1 diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 1c8713978d..527cbda2ff 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-awssdk-v2 jooby-awssdk-v2 diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index 4190e6e211..e7bf79f39d 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -7,14 +7,14 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT io.jooby jooby-bom jooby-bom pom - 4.0.15 + 4.0.16-SNAPSHOT Jooby (Bill of Materials) https://jooby.io diff --git a/modules/jooby-caffeine/pom.xml b/modules/jooby-caffeine/pom.xml index 80b4e273a0..6e7423414a 100644 --- a/modules/jooby-caffeine/pom.xml +++ b/modules/jooby-caffeine/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-caffeine jooby-caffeine diff --git a/modules/jooby-camel/pom.xml b/modules/jooby-camel/pom.xml index 616051e905..d38094d3e5 100644 --- a/modules/jooby-camel/pom.xml +++ b/modules/jooby-camel/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-camel jooby-camel diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 487960e9d2..5137404098 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-cli jooby-cli diff --git a/modules/jooby-commons-email/pom.xml b/modules/jooby-commons-email/pom.xml index d8d8e91dd9..26720bd407 100644 --- a/modules/jooby-commons-email/pom.xml +++ b/modules/jooby-commons-email/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-commons-email jooby-commons-email diff --git a/modules/jooby-conscrypt/pom.xml b/modules/jooby-conscrypt/pom.xml index 7e71c02e00..51d54a1816 100644 --- a/modules/jooby-conscrypt/pom.xml +++ b/modules/jooby-conscrypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-conscrypt jooby-conscrypt diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index bf3fd6f4c3..032032caf8 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-db-scheduler jooby-db-scheduler diff --git a/modules/jooby-distribution/pom.xml b/modules/jooby-distribution/pom.xml index c7530daa65..4c9726d594 100644 --- a/modules/jooby-distribution/pom.xml +++ b/modules/jooby-distribution/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-distribution jooby-distribution diff --git a/modules/jooby-ebean/pom.xml b/modules/jooby-ebean/pom.xml index 20c0244eda..f956649071 100644 --- a/modules/jooby-ebean/pom.xml +++ b/modules/jooby-ebean/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-ebean jooby-ebean diff --git a/modules/jooby-flyway/pom.xml b/modules/jooby-flyway/pom.xml index d6f81599a9..5c65e116c6 100644 --- a/modules/jooby-flyway/pom.xml +++ b/modules/jooby-flyway/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-flyway jooby-flyway diff --git a/modules/jooby-freemarker/pom.xml b/modules/jooby-freemarker/pom.xml index 5fe7dcb731..bd72137714 100644 --- a/modules/jooby-freemarker/pom.xml +++ b/modules/jooby-freemarker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-freemarker jooby-freemarker diff --git a/modules/jooby-gradle-setup/pom.xml b/modules/jooby-gradle-setup/pom.xml index d05398b08a..760ccc3bef 100644 --- a/modules/jooby-gradle-setup/pom.xml +++ b/modules/jooby-gradle-setup/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-gradle-setup jooby-gradle-setup diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index db614e85b7..fa1f6fa339 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-graphiql jooby-graphiql diff --git a/modules/jooby-graphql/pom.xml b/modules/jooby-graphql/pom.xml index 97144813cb..ca8504fadd 100644 --- a/modules/jooby-graphql/pom.xml +++ b/modules/jooby-graphql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-graphql jooby-graphql diff --git a/modules/jooby-gson/pom.xml b/modules/jooby-gson/pom.xml index 6b6c0c9c1c..ecb107fb71 100644 --- a/modules/jooby-gson/pom.xml +++ b/modules/jooby-gson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-gson jooby-gson diff --git a/modules/jooby-guice/pom.xml b/modules/jooby-guice/pom.xml index 7945de2bf4..3727caa06d 100644 --- a/modules/jooby-guice/pom.xml +++ b/modules/jooby-guice/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-guice jooby-guice diff --git a/modules/jooby-handlebars/pom.xml b/modules/jooby-handlebars/pom.xml index ec3c0a2790..0be10f9bd8 100644 --- a/modules/jooby-handlebars/pom.xml +++ b/modules/jooby-handlebars/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-handlebars jooby-handlebars diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index 033e964197..d325a250c9 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-hibernate-validator jooby-hibernate-validator diff --git a/modules/jooby-hibernate/pom.xml b/modules/jooby-hibernate/pom.xml index 798ae17711..dda58e29fc 100644 --- a/modules/jooby-hibernate/pom.xml +++ b/modules/jooby-hibernate/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-hibernate jooby-hibernate diff --git a/modules/jooby-hikari/pom.xml b/modules/jooby-hikari/pom.xml index 9caf6c822d..31304ca027 100644 --- a/modules/jooby-hikari/pom.xml +++ b/modules/jooby-hikari/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-hikari jooby-hikari diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index aa491b44a9..9243f83b01 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jackson jooby-jackson diff --git a/modules/jooby-jasypt/pom.xml b/modules/jooby-jasypt/pom.xml index 741a18b13e..8dfcc482cf 100644 --- a/modules/jooby-jasypt/pom.xml +++ b/modules/jooby-jasypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jasypt jooby-jasypt diff --git a/modules/jooby-jdbi/pom.xml b/modules/jooby-jdbi/pom.xml index 2f9fd91b85..9af07a768e 100644 --- a/modules/jooby-jdbi/pom.xml +++ b/modules/jooby-jdbi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jdbi jooby-jdbi diff --git a/modules/jooby-jetty/pom.xml b/modules/jooby-jetty/pom.xml index 659a383054..9243dc20d1 100644 --- a/modules/jooby-jetty/pom.xml +++ b/modules/jooby-jetty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jetty jooby-jetty diff --git a/modules/jooby-jstachio/pom.xml b/modules/jooby-jstachio/pom.xml index 5e69224d1e..08a979fa18 100644 --- a/modules/jooby-jstachio/pom.xml +++ b/modules/jooby-jstachio/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jstachio jooby-jstachio diff --git a/modules/jooby-jte/pom.xml b/modules/jooby-jte/pom.xml index 20639635fb..baa6b177fe 100644 --- a/modules/jooby-jte/pom.xml +++ b/modules/jooby-jte/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jte jooby-jte diff --git a/modules/jooby-jwt/pom.xml b/modules/jooby-jwt/pom.xml index cbbf08cc1b..ac3f3e22ac 100644 --- a/modules/jooby-jwt/pom.xml +++ b/modules/jooby-jwt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-jwt jooby-jwt diff --git a/modules/jooby-kafka/pom.xml b/modules/jooby-kafka/pom.xml index d60ddb258b..4b5bab018f 100644 --- a/modules/jooby-kafka/pom.xml +++ b/modules/jooby-kafka/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-kafka jooby-kafka diff --git a/modules/jooby-kotlin/pom.xml b/modules/jooby-kotlin/pom.xml index 3d26f8fba9..680e567fa2 100644 --- a/modules/jooby-kotlin/pom.xml +++ b/modules/jooby-kotlin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-kotlin jooby-kotlin diff --git a/modules/jooby-log4j/pom.xml b/modules/jooby-log4j/pom.xml index 4282d7e726..82d2da3ba0 100644 --- a/modules/jooby-log4j/pom.xml +++ b/modules/jooby-log4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-log4j jooby-log4j diff --git a/modules/jooby-logback/pom.xml b/modules/jooby-logback/pom.xml index 27baecd5bc..3650cd6387 100644 --- a/modules/jooby-logback/pom.xml +++ b/modules/jooby-logback/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-logback jooby-logback diff --git a/modules/jooby-maven-plugin/pom.xml b/modules/jooby-maven-plugin/pom.xml index b226646ce7..c528241c9e 100644 --- a/modules/jooby-maven-plugin/pom.xml +++ b/modules/jooby-maven-plugin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-maven-plugin jooby-maven-plugin diff --git a/modules/jooby-metrics/pom.xml b/modules/jooby-metrics/pom.xml index 0905ce3b65..b92d9d265b 100644 --- a/modules/jooby-metrics/pom.xml +++ b/modules/jooby-metrics/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-metrics jooby-metrics diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 6407b95a83..61218f746c 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-mutiny jooby-mutiny diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index 3a9031fb97..5494b466d3 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-netty jooby-netty diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 6deaaca0bf..79a0a099ab 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-openapi jooby-openapi diff --git a/modules/jooby-pac4j/pom.xml b/modules/jooby-pac4j/pom.xml index 949d2acd4f..ab62f244cb 100644 --- a/modules/jooby-pac4j/pom.xml +++ b/modules/jooby-pac4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-pac4j jooby-pac4j diff --git a/modules/jooby-pebble/pom.xml b/modules/jooby-pebble/pom.xml index 834f47792e..e15d64ecf0 100644 --- a/modules/jooby-pebble/pom.xml +++ b/modules/jooby-pebble/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-pebble jooby-pebble diff --git a/modules/jooby-quartz/pom.xml b/modules/jooby-quartz/pom.xml index 22b521f0cc..384744501a 100644 --- a/modules/jooby-quartz/pom.xml +++ b/modules/jooby-quartz/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-quartz jooby-quartz diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index f80e683e5d..77e95314b1 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-reactor jooby-reactor diff --git a/modules/jooby-redis/pom.xml b/modules/jooby-redis/pom.xml index a31ffeceda..3c0ab9180a 100644 --- a/modules/jooby-redis/pom.xml +++ b/modules/jooby-redis/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-redis jooby-redis diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index 8592be6a29..b8b8ff5c86 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-redoc jooby-redoc diff --git a/modules/jooby-rocker/pom.xml b/modules/jooby-rocker/pom.xml index 0250eadecf..7ea9e60644 100644 --- a/modules/jooby-rocker/pom.xml +++ b/modules/jooby-rocker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-rocker jooby-rocker diff --git a/modules/jooby-run/pom.xml b/modules/jooby-run/pom.xml index 49e3355f83..ad6818201a 100644 --- a/modules/jooby-run/pom.xml +++ b/modules/jooby-run/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-run jooby-run diff --git a/modules/jooby-rxjava3/pom.xml b/modules/jooby-rxjava3/pom.xml index 582e90d54b..77293a94b1 100644 --- a/modules/jooby-rxjava3/pom.xml +++ b/modules/jooby-rxjava3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-rxjava3 jooby-rxjava3 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index adc6e01cfa..992ede0870 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -4,7 +4,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-stork diff --git a/modules/jooby-swagger-ui/pom.xml b/modules/jooby-swagger-ui/pom.xml index c269902298..0c265abf83 100644 --- a/modules/jooby-swagger-ui/pom.xml +++ b/modules/jooby-swagger-ui/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-swagger-ui jooby-swagger-ui diff --git a/modules/jooby-test/pom.xml b/modules/jooby-test/pom.xml index d4603bd494..d68ff99fc4 100644 --- a/modules/jooby-test/pom.xml +++ b/modules/jooby-test/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-test jooby-test diff --git a/modules/jooby-thymeleaf/pom.xml b/modules/jooby-thymeleaf/pom.xml index 30b6a341cf..9674129360 100644 --- a/modules/jooby-thymeleaf/pom.xml +++ b/modules/jooby-thymeleaf/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-thymeleaf jooby-thymeleaf diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index 6dd7648e29..32d5c2d1ec 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-undertow jooby-undertow diff --git a/modules/jooby-vertx-mysql-client/pom.xml b/modules/jooby-vertx-mysql-client/pom.xml index 8ae32ff54a..5e56a2e4b2 100644 --- a/modules/jooby-vertx-mysql-client/pom.xml +++ b/modules/jooby-vertx-mysql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-vertx-mysql-client jooby-vertx-mysql-client diff --git a/modules/jooby-vertx-pg-client/pom.xml b/modules/jooby-vertx-pg-client/pom.xml index 80a903abf1..00413a3841 100644 --- a/modules/jooby-vertx-pg-client/pom.xml +++ b/modules/jooby-vertx-pg-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-vertx-pg-client jooby-vertx-pg-client diff --git a/modules/jooby-vertx-sql-client/pom.xml b/modules/jooby-vertx-sql-client/pom.xml index 0ed5bf53ce..b548edf5a2 100644 --- a/modules/jooby-vertx-sql-client/pom.xml +++ b/modules/jooby-vertx-sql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-vertx-sql-client jooby-vertx-sql-client diff --git a/modules/jooby-vertx/pom.xml b/modules/jooby-vertx/pom.xml index 167eb62c56..9a4244574f 100644 --- a/modules/jooby-vertx/pom.xml +++ b/modules/jooby-vertx/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-vertx jooby-vertx diff --git a/modules/jooby-whoops/pom.xml b/modules/jooby-whoops/pom.xml index 45a39b784f..b0ec404392 100644 --- a/modules/jooby-whoops/pom.xml +++ b/modules/jooby-whoops/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-whoops jooby-whoops diff --git a/modules/jooby-yasson/pom.xml b/modules/jooby-yasson/pom.xml index f60d011ed4..8388131438 100644 --- a/modules/jooby-yasson/pom.xml +++ b/modules/jooby-yasson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.15 + 4.0.16-SNAPSHOT jooby-yasson jooby-yasson diff --git a/modules/pom.xml b/modules/pom.xml index 3d86995753..8af7217521 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ io.jooby jooby-project - 4.0.15 + 4.0.16-SNAPSHOT modules diff --git a/pom.xml b/pom.xml index d53ff154d1..279efa7ba1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jooby jooby-project - 4.0.15 + 4.0.16-SNAPSHOT pom jooby-project @@ -210,7 +210,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2026-01-14T14:25:56Z + 2026-01-14T14:52:09Z UTF-8 etc${file.separator}source${file.separator}formatter.sh diff --git a/tests/pom.xml b/tests/pom.xml index 9cb452738a..ba12aa93a7 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.15 + 4.0.16-SNAPSHOT tests tests From 39d1112acbe1ac9b1686c070ec472a7465e62e86 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Thu, 15 Jan 2026 10:00:54 +0200 Subject: [PATCH 02/31] support `jakarta.ws.rs.DefaultValue` handling --- .../io/jooby/internal/apt/MvcParameter.java | 4 +- .../internal/apt/ParameterGenerator.java | 37 ++++++++++++++----- .../test/java/tests/i3761/C3761Jakarta.java | 35 ++++++++++++++++++ .../src/test/java/tests/i3761/Issue3761.java | 30 +++++++++------ 4 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java index 00bdef21da..107f177516 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java @@ -98,13 +98,13 @@ yield hasAnnotation(NULLABLE) if (strategy.isEmpty()) { // must be body yield ParameterGenerator.BodyParam.toSourceCode( - kt, route, null, type, parameterName, isNullable(kt)); + kt, route, null, type, parameter, parameterName, isNullable(kt)); } else { var paramGenerator = strategy.get().getKey(); paramGenerator.verifyType(parameterType, parameterName, route); yield paramGenerator.toSourceCode( - kt, route, strategy.get().getValue(), type, parameterName, isNullable(kt)); + kt, route, strategy.get().getValue(), type, parameter, parameterName, isNullable(kt)); } } }; diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java index d99d8942e1..34f1e07d9b 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java @@ -5,15 +5,14 @@ */ package io.jooby.internal.apt; -import static io.jooby.internal.apt.AnnotationSupport.findAnnotationValue; -import static io.jooby.internal.apt.Types.BUILT_IN; -import static java.util.stream.Collectors.joining; - +import javax.lang.model.element.*; import java.util.*; import java.util.function.Predicate; import java.util.stream.Stream; -import javax.lang.model.element.*; +import static io.jooby.internal.apt.AnnotationSupport.findAnnotationValue; +import static io.jooby.internal.apt.Types.BUILT_IN; +import static java.util.stream.Collectors.joining; public enum ParameterGenerator { ContextParam("getAttribute", "io.jooby.annotation.ContextParam", "jakarta.ws.rs.core.Context") { @@ -23,6 +22,7 @@ public String toSourceCode( MvcRoute route, AnnotationMirror annotation, TypeDefinition type, + VariableElement parameter, String name, boolean nullable) { if (type.is(Map.class)) { @@ -70,6 +70,7 @@ public String toSourceCode( MvcRoute route, AnnotationMirror annotation, TypeDefinition type, + VariableElement parameter, String name, boolean nullable) { var rawType = type.getRawType().toString(); @@ -107,6 +108,7 @@ public String toSourceCode( MvcRoute route, AnnotationMirror annotation, TypeDefinition type, + VariableElement parameter, String name, boolean nullable) { List converters = new ArrayList<>(); @@ -200,10 +202,11 @@ public String toSourceCode( MvcRoute route, AnnotationMirror annotation, TypeDefinition type, + VariableElement parameter, String name, boolean nullable) { var paramSource = source(annotation); - var builtin = builtinType(kt, annotation, type, name, nullable); + var builtin = builtinType(kt, annotation, type, parameter, name, nullable); if (builtin == null) { // List, Set, var toValue = @@ -323,10 +326,15 @@ public String toSourceCode( } protected String builtinType( - boolean kt, AnnotationMirror annotation, TypeDefinition type, String name, boolean nullable) { + boolean kt, + AnnotationMirror annotation, + TypeDefinition type, + VariableElement parameter, + String name, + boolean nullable) { if (BUILT_IN.stream().anyMatch(type::is)) { var paramSource = source(annotation); - var defaultValue = defaultValue(annotation); + var defaultValue = defaultValue(parameter, annotation); // look at named parameter if (type.isPrimitive()) { // like: .intValue @@ -425,10 +433,19 @@ protected String source(AnnotationMirror annotation) { return ""; } - protected String defaultValue(AnnotationMirror annotation) { - if (annotation.getAnnotationType().toString().startsWith("io.jooby.annotation")) { + protected String defaultValue(VariableElement parameter, AnnotationMirror annotation) { + var annotationType = annotation.getAnnotationType().toString(); + + if (annotationType.startsWith("io.jooby.annotation")) { var sources = findAnnotationValue(annotation, AnnotationSupport.VALUE); return sources.isEmpty() ? "" : CodeBlock.of(", ", CodeBlock.string(sources.getFirst())); + } else if (annotationType.startsWith("jakarta.ws.rs")) { + var defaultValueAnnotation = AnnotationSupport.findAnnotationByName( + parameter, "jakarta.ws.rs.DefaultValue"); + if (defaultValueAnnotation != null) { + var defaultValue = findAnnotationValue(defaultValueAnnotation, AnnotationSupport.VALUE); + return defaultValue.isEmpty() ? "" : CodeBlock.of(", ", CodeBlock.string(defaultValue.getFirst())); + } } return ""; } diff --git a/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java b/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java new file mode 100644 index 0000000000..4802038a84 --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java @@ -0,0 +1,35 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package tests.i3761; + +import io.jooby.annotation.GET; +import io.jooby.annotation.Path; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.QueryParam; + +@Path("/3761") +public class C3761Jakarta { + + @GET("/number") + public int number(@QueryParam("num") @DefaultValue("5") int num) { + return num; + } + + @GET("/unset") + public String unset(@QueryParam("unset") String unset) { + return unset; + } + + @GET("/emptySet") + public String emptySet(@QueryParam("emptySet") @DefaultValue("") String emptySet) { + return emptySet; + } + + @GET("/stringVal") + public String string(@QueryParam("stringVal") @DefaultValue("Hello") String stringVal) { + return stringVal; + } +} diff --git a/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java b/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java index f8347154b1..6a18f83c1b 100644 --- a/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java +++ b/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java @@ -5,24 +5,30 @@ */ package tests.i3761; -import static org.junit.jupiter.api.Assertions.*; - +import io.jooby.apt.ProcessorRunner; import org.junit.jupiter.api.Test; -import io.jooby.apt.ProcessorRunner; +import static org.junit.jupiter.api.Assertions.assertTrue; public class Issue3761 { @Test public void shouldGenerateDefaultValues() throws Exception { new ProcessorRunner(new C3761()) - .withSourceCode( - (source) -> { - assertTrue(source.contains("return c.number(ctx.query(\"num\", \"5\").intValue());")); - assertTrue(source.contains("return c.unset(ctx.query(\"unset\").valueOrNull());")); - assertTrue( - source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());")); - assertTrue( - source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());")); - }); + .withSourceCode(Issue3761::assertSourceCodeRespectDefaultValues); + } + + @Test + public void shouldGenerateJakartaDefaultValues() throws Exception { + new ProcessorRunner(new C3761Jakarta()) + .withSourceCode(Issue3761::assertSourceCodeRespectDefaultValues); + } + + private static void assertSourceCodeRespectDefaultValues(String source) { + assertTrue(source.contains("return c.number(ctx.query(\"num\", \"5\").intValue());")); + assertTrue(source.contains("return c.unset(ctx.query(\"unset\").valueOrNull());")); + assertTrue( + source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());")); + assertTrue( + source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());")); } } From 0bad11cca4c31962e08f1d45a573bbc9cdfcf6fe Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Thu, 15 Jan 2026 16:02:13 +0200 Subject: [PATCH 03/31] open-api: support `jakarta.ws.rs.DefaultValue` handling --- .../internal/openapi/AnnotationParser.java | 14 +++++ .../java/issues/i3835/App3835Jakarta.java | 16 ++++++ .../test/java/issues/i3835/C3835Jakarta.java | 33 ++++++++++++ .../src/test/java/issues/i3835/Issue3835.java | 52 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 modules/jooby-openapi/src/test/java/issues/i3835/App3835Jakarta.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3835/C3835Jakarta.java diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java index 30a0b82a73..27edcd6b71 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java @@ -122,6 +122,20 @@ public Optional getDefaultValue(List annotations) { } } } + return getJakartaDefaultValue(annotations); + } + + public Optional getJakartaDefaultValue(List annotations) { + List names = + Stream.of(annotations()).filter(it -> it.getName().startsWith("jakarta.ws.rs")).toList(); + for (var a : annotations) { + if (a.values != null) { + var matches = names.stream().anyMatch(it -> "Ljakarta/ws/rs/DefaultValue;".equals(a.desc)); + if (matches) { + return AnnotationUtils.findAnnotationValue(a, "value").map(Objects::toString); + } + } + } return Optional.empty(); } diff --git a/modules/jooby-openapi/src/test/java/issues/i3835/App3835Jakarta.java b/modules/jooby-openapi/src/test/java/issues/i3835/App3835Jakarta.java new file mode 100644 index 0000000000..300ae3631c --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3835/App3835Jakarta.java @@ -0,0 +1,16 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3835; + +import io.jooby.Jooby; + +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + +public class App3835Jakarta extends Jooby { + { + mvc(toMvcExtension(C3835Jakarta.class)); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3835/C3835Jakarta.java b/modules/jooby-openapi/src/test/java/issues/i3835/C3835Jakarta.java new file mode 100644 index 0000000000..1734cef8bc --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3835/C3835Jakarta.java @@ -0,0 +1,33 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3835; + +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +import java.util.List; +import java.util.Map; + +@Path("/3835") +public class C3835Jakarta { + + /** + * Search/scan index. + * + * @param q Search string. Defaults to * + * @return Search result. + */ + @GET + public Map search( + @QueryParam("q") @DefaultValue("*") String q, + @QueryParam("pageSize") @DefaultValue("20") int pageSize, + @QueryParam("page") @DefaultValue("1") int page, + @QueryParam("options") @DefaultValue("--a") List options) { + return null; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3835/Issue3835.java b/modules/jooby-openapi/src/test/java/issues/i3835/Issue3835.java index a14c78f23e..ad0ff6aeb8 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3835/Issue3835.java +++ b/modules/jooby-openapi/src/test/java/issues/i3835/Issue3835.java @@ -63,4 +63,56 @@ public void shouldGenerateCorrectName(OpenAPIResult result) { type: object\ """); } + + @OpenAPITest(value = App3835Jakarta.class) + public void shouldGenerateJakartaDefaultValues(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.0.1 + info: + title: 3835Jakarta API + description: 3835Jakarta API description + version: "1.0" + paths: + /3835: + get: + summary: Search/scan index. + operationId: search + parameters: + - name: q + in: query + description: Search string. Defaults to * + schema: + type: string + default: '*' + - name: pageSize + in: query + schema: + type: integer + format: int32 + default: 20 + - name: page + in: query + schema: + type: integer + format: int32 + default: 1 + - name: options + in: query + schema: + type: array + items: + type: string + responses: + "200": + description: Search result. + content: + application/json: + schema: + type: object + additionalProperties: + type: object\ + """); + } } From 885c8bfd9b93ace0032f80d0f14155cf1dda919c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:33:04 +0000 Subject: [PATCH 04/31] build(deps): bump the dependencies group across 1 directory with 18 updates Bumps the dependencies group with 18 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) | `2.20.1` | `2.21.0` | | [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) | `1.5.24` | `1.5.25` | | io.swagger.core.v3:swagger-annotations | `2.2.41` | `2.2.42` | | io.swagger.core.v3:swagger-models | `2.2.41` | `2.2.42` | | [io.undertow:undertow-core](https://github.com/undertow-io/undertow) | `2.3.21.Final` | `2.3.22.Final` | | org.jboss.modules:jboss-modules | `2.2.0.Final` | `2.3.0` | | [io.repaint.maven:tiles-maven-plugin](https://github.com/repaint-io/maven-tiles) | `2.42` | `2.43` | | [org.codehaus.mojo:versions-maven-plugin](https://github.com/mojohaus/versions) | `2.20.1` | `2.21.0` | | [io.vertx:vertx-core](https://github.com/eclipse/vert.x) | `5.0.6` | `5.0.7` | | [io.vertx:vertx-sql-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.6` | `5.0.7` | | [io.vertx:vertx-mysql-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.6` | `5.0.7` | | [io.vertx:vertx-pg-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.6` | `5.0.7` | | [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) | `1.18.3` | `1.18.4` | | [gg.jte:jte](https://github.com/casid/jte) | `3.2.1` | `3.2.2` | | [gg.jte:jte-models](https://github.com/casid/jte) | `3.2.1` | `3.2.2` | | software.amazon.awssdk:bom | `2.41.5` | `2.41.11` | | [org.codehaus.mojo:properties-maven-plugin](https://github.com/mojohaus/properties-maven-plugin) | `1.2.1` | `1.3.0` | | [io.projectreactor:reactor-core](https://github.com/reactor/reactor-core) | `3.8.1` | `3.8.2` | Updates `com.fasterxml.jackson:jackson-bom` from 2.20.1 to 2.21.0 - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.20.1...jackson-bom-2.21.0) Updates `ch.qos.logback:logback-classic` from 1.5.24 to 1.5.25 - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.24...v_1.5.25) Updates `io.swagger.core.v3:swagger-annotations` from 2.2.41 to 2.2.42 Updates `io.swagger.core.v3:swagger-models` from 2.2.41 to 2.2.42 Updates `io.swagger.core.v3:swagger-models` from 2.2.41 to 2.2.42 Updates `io.undertow:undertow-core` from 2.3.21.Final to 2.3.22.Final - [Release notes](https://github.com/undertow-io/undertow/releases) - [Commits](https://github.com/undertow-io/undertow/compare/2.3.21.Final...2.3.22.Final) Updates `org.jboss.modules:jboss-modules` from 2.2.0.Final to 2.3.0 Updates `io.repaint.maven:tiles-maven-plugin` from 2.42 to 2.43 - [Release notes](https://github.com/repaint-io/maven-tiles/releases) - [Changelog](https://github.com/repaint-io/maven-tiles/blob/master/CHANGELOG.adoc) - [Commits](https://github.com/repaint-io/maven-tiles/compare/tiles-maven-plugin-2.42...tiles-maven-plugin-2.43) Updates `org.codehaus.mojo:versions-maven-plugin` from 2.20.1 to 2.21.0 - [Release notes](https://github.com/mojohaus/versions/releases) - [Changelog](https://github.com/mojohaus/versions/blob/master/ReleaseNotes.md) - [Commits](https://github.com/mojohaus/versions/compare/2.20.1...2.21.0) Updates `io.vertx:vertx-core` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse/vert.x/compare/5.0.6...5.0.7) Updates `io.vertx:vertx-sql-client` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.6...5.0.7) Updates `io.vertx:vertx-mysql-client` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.6...5.0.7) Updates `io.vertx:vertx-pg-client` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.6...5.0.7) Updates `net.bytebuddy:byte-buddy` from 1.18.3 to 1.18.4 - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.3...byte-buddy-1.18.4) Updates `io.vertx:vertx-sql-client` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.6...5.0.7) Updates `io.vertx:vertx-mysql-client` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.6...5.0.7) Updates `io.vertx:vertx-pg-client` from 5.0.6 to 5.0.7 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.6...5.0.7) Updates `gg.jte:jte` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/casid/jte/releases) - [Commits](https://github.com/casid/jte/compare/3.2.1...3.2.2) Updates `gg.jte:jte-models` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/casid/jte/releases) - [Commits](https://github.com/casid/jte/compare/3.2.1...3.2.2) Updates `gg.jte:jte-models` from 3.2.1 to 3.2.2 - [Release notes](https://github.com/casid/jte/releases) - [Commits](https://github.com/casid/jte/compare/3.2.1...3.2.2) Updates `software.amazon.awssdk:bom` from 2.41.5 to 2.41.11 Updates `org.codehaus.mojo:properties-maven-plugin` from 1.2.1 to 1.3.0 - [Release notes](https://github.com/mojohaus/properties-maven-plugin/releases) - [Commits](https://github.com/mojohaus/properties-maven-plugin/compare/1.2.1...properties-maven-plugin-1.3.0) Updates `io.projectreactor:reactor-core` from 3.8.1 to 3.8.2 - [Release notes](https://github.com/reactor/reactor-core/releases) - [Commits](https://github.com/reactor/reactor-core/compare/v3.8.1...v3.8.2) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.5.25 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-annotations dependency-version: 2.2.42 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.42 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.42 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.undertow:undertow-core dependency-version: 2.3.22.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jboss.modules:jboss-modules dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.repaint.maven:tiles-maven-plugin dependency-version: '2.43' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.codehaus.mojo:versions-maven-plugin dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.vertx:vertx-core dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-sql-client dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-mysql-client dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-pg-client dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-sql-client dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-mysql-client dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-pg-client dependency-version: 5.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: gg.jte:jte dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: gg.jte:jte-models dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: gg.jte:jte-models dependency-version: 3.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.41.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.codehaus.mojo:properties-maven-plugin dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.projectreactor:reactor-core dependency-version: 3.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- pom.xml | 20 ++++++++++---------- tests/pom.xml | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 527cbda2ff..4d8cb23969 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.41.5 + 2.41.11 diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 5137404098..5f2a666d6b 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -94,7 +94,7 @@ org.codehaus.mojo properties-maven-plugin - 1.2.1 + 1.3.0 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 79a0a099ab..7472f5b884 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -156,7 +156,7 @@ net.bytebuddy byte-buddy - 1.18.3 + 1.18.4 test diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index 77e95314b1..bfe220da23 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -26,7 +26,7 @@ io.projectreactor reactor-core - 3.8.1 + 3.8.2 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index 992ede0870..3fb64597b0 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -20,7 +20,7 @@ io.repaint.maven tiles-maven-plugin - 2.42 + 2.43 true true diff --git a/pom.xml b/pom.xml index 279efa7ba1..861a31dcb9 100644 --- a/pom.xml +++ b/pom.xml @@ -61,13 +61,13 @@ 4.5.0 1.3.7 4.1.0 - 2.20.1 + 2.21.0 2.13.2 3.0.1 3.0.4 2.4.0 3.1.3.RELEASE - 3.2.1 + 3.2.2 7.0.2 @@ -88,7 +88,7 @@ 7.0.0 - 1.5.24 + 1.5.25 2.25.3 2.0.17 @@ -99,19 +99,19 @@ 4.2.37 - 2.2.0.Final + 2.3.0 9.9.1 - 2.3.21.Final + 2.3.22.Final 12.1.5 4.2.9.Final - 5.0.6 + 5.0.7 - 2.2.41 + 2.2.42 2.1.37 2.0.0-rc.20 @@ -171,7 +171,7 @@ 3.2.0 3.2.0 3.8.0 - 2.42 + 2.43 3.14.1 3.9.12 3.6.2 @@ -191,7 +191,7 @@ 2.3.1 4.0.2 3.2.0 - 2.20.1 + 2.21.0 3.6.3 3.1.2 2.0.0 @@ -1654,7 +1654,7 @@ org.codehaus.mojo versions-maven-plugin - 2.20.1 + 2.21.0 diff --git a/tests/pom.xml b/tests/pom.xml index ba12aa93a7..5c2189d625 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -206,7 +206,7 @@ io.vertx vertx-pg-client - 5.0.6 + 5.0.7 From 2e349447e3415d6d7a36b8c719b887b96a8d1ee8 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 24 Jan 2026 17:23:23 -0500 Subject: [PATCH 05/31] update readme --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e44c538c65..b64141d55f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,24 @@ # ∞ do more, more easily -[Jooby](https://jooby.io) is a modern, performant and easy to use web framework for Java and Kotlin built on top of your -favorite web server. +[Jooby](https://jooby.io) is a modern, high-performance web framework for Java and Kotlin, designed to run seamlessly atop your preferred web server. + +## 🚀 Built for Speed +- **High Performance**: Consistently ranks among the fastest Java frameworks in TechEmpower benchmarks. +- **Lightweight Footprint**: Low memory usage and fast startup times make it ideal for microservices and serverless environments. +- **Choose Your Engine**: Built to run on your favorite high-performance servers: Netty, Jetty, or Undertow. + +## 🛠️ Developer Productivity +- **Instant Hot-Reload**: Save your code and see changes immediately without restarting the entire JVM. +- **Modular by Design**: Only use what you need. Jooby offers over 50 "thin" modules for database access (Hibernate, JDBI, Flyway), security (Pac4j), and more. +- **OpenAPI & Swagger**: Automatically generate interactive documentation for your APIs with built-in OpenAPI 3 support. + +## 🧩 Unrivaled Flexibility +- **The Power of Choice**: Use the Script API (fluent, lambda-based routes) for simple apps, or the MVC API (annotation-based) for complex enterprise projects. +- **Reactive & Non-Blocking**: Full support for modern async patterns, including Kotlin Coroutines, RxJava, Reactor, and CompletableFutures. +- **First-Class Kotlin Support**: Native DSLs and features designed specifically to make Kotlin development feel intuitive and type-safe. + +## Quick Start Java: @@ -72,11 +88,6 @@ Previous version - v2: [Documentation](https://jooby.io/v2) and [source code](https://github.com/jooby-project/jooby/tree/2.x) - v1: [Documentation](https://jooby.io/v1) and [source code](https://github.com/jooby-project/jooby/tree/1.x) -author -===== - - [Edgar Espina](https://twitter.com/edgarespina) - license ===== From 309585802c0177d4f32c240f81c9730c6a6bd097 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 24 Jan 2026 17:37:50 -0500 Subject: [PATCH 06/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b64141d55f..17d1c4d128 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## 🚀 Built for Speed - **High Performance**: Consistently ranks among the fastest Java frameworks in TechEmpower benchmarks. -- **Lightweight Footprint**: Low memory usage and fast startup times make it ideal for microservices and serverless environments. +- **Lightweight Footprint**: Low memory usage and fast startup times make it ideal for microservices environments. - **Choose Your Engine**: Built to run on your favorite high-performance servers: Netty, Jetty, or Undertow. ## 🛠️ Developer Productivity From 18e088db779cd12d4df627d5cfb2fd107a89404d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:07:51 +0000 Subject: [PATCH 07/31] build(deps): bump the dependencies group with 11 updates Bumps the dependencies group with 11 updates: | Package | From | To | | --- | --- | --- | | [io.avaje:avaje-inject](https://github.com/avaje/avaje-inject) | `12.2` | `12.3` | | io.avaje:avaje-inject-generator | `12.2` | `12.3` | | [io.avaje:avaje-jsonb](https://github.com/avaje/avaje-jsonb) | `3.9` | `3.10` | | io.avaje:avaje-jsonb-generator | `3.9` | `3.10` | | [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) | `1.5.25` | `1.5.26` | | [io.dropwizard.metrics:metrics-core](https://github.com/dropwizard/metrics) | `4.2.37` | `4.2.38` | | [io.dropwizard.metrics:metrics-healthchecks](https://github.com/dropwizard/metrics) | `4.2.37` | `4.2.38` | | [io.dropwizard.metrics:metrics-jvm](https://github.com/dropwizard/metrics) | `4.2.37` | `4.2.38` | | [org.assertj:assertj-core](https://github.com/assertj/assertj) | `3.27.6` | `3.27.7` | | [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) | `3.1.0` | `3.2.0` | | software.amazon.awssdk:bom | `2.41.11` | `2.41.14` | Updates `io.avaje:avaje-inject` from 12.2 to 12.3 - [Release notes](https://github.com/avaje/avaje-inject/releases) - [Commits](https://github.com/avaje/avaje-inject/compare/12.2...12.3) Updates `io.avaje:avaje-inject-generator` from 12.2 to 12.3 Updates `io.avaje:avaje-inject-generator` from 12.2 to 12.3 Updates `io.avaje:avaje-jsonb` from 3.9 to 3.10 - [Release notes](https://github.com/avaje/avaje-jsonb/releases) - [Commits](https://github.com/avaje/avaje-jsonb/compare/3.9...3.10) Updates `io.avaje:avaje-jsonb-generator` from 3.9 to 3.10 Updates `io.avaje:avaje-jsonb-generator` from 3.9 to 3.10 Updates `ch.qos.logback:logback-classic` from 1.5.25 to 1.5.26 - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.25...v_1.5.26) Updates `io.dropwizard.metrics:metrics-core` from 4.2.37 to 4.2.38 - [Release notes](https://github.com/dropwizard/metrics/releases) - [Commits](https://github.com/dropwizard/metrics/compare/v4.2.37...v4.2.38) Updates `io.dropwizard.metrics:metrics-healthchecks` from 4.2.37 to 4.2.38 - [Release notes](https://github.com/dropwizard/metrics/releases) - [Commits](https://github.com/dropwizard/metrics/compare/v4.2.37...v4.2.38) Updates `io.dropwizard.metrics:metrics-jvm` from 4.2.37 to 4.2.38 - [Release notes](https://github.com/dropwizard/metrics/releases) - [Commits](https://github.com/dropwizard/metrics/compare/v4.2.37...v4.2.38) Updates `io.dropwizard.metrics:metrics-healthchecks` from 4.2.37 to 4.2.38 - [Release notes](https://github.com/dropwizard/metrics/releases) - [Commits](https://github.com/dropwizard/metrics/compare/v4.2.37...v4.2.38) Updates `io.dropwizard.metrics:metrics-jvm` from 4.2.37 to 4.2.38 - [Release notes](https://github.com/dropwizard/metrics/releases) - [Commits](https://github.com/dropwizard/metrics/compare/v4.2.37...v4.2.38) Updates `org.assertj:assertj-core` from 3.27.6 to 3.27.7 - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.6...assertj-build-3.27.7) Updates `com.diffplug.spotless:spotless-maven-plugin` from 3.1.0 to 3.2.0 - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/lib/3.1.0...lib/3.2.0) Updates `software.amazon.awssdk:bom` from 2.41.11 to 2.41.14 --- updated-dependencies: - dependency-name: io.avaje:avaje-inject dependency-version: '12.3' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.3' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.3' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb dependency-version: '3.10' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.10' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.10' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.5.26 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.dropwizard.metrics:metrics-core dependency-version: 4.2.38 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.dropwizard.metrics:metrics-healthchecks dependency-version: 4.2.38 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.dropwizard.metrics:metrics-jvm dependency-version: 4.2.38 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.dropwizard.metrics:metrics-healthchecks dependency-version: 4.2.38 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.dropwizard.metrics:metrics-jvm dependency-version: 4.2.38 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.assertj:assertj-core dependency-version: 3.27.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.diffplug.spotless:spotless-maven-plugin dependency-version: 3.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.41.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 4 ++-- pom.xml | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index 971b651aad..16f68c8702 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -88,7 +88,7 @@ org.assertj assertj-core - 3.27.6 + 3.27.7 test diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 4d8cb23969..a66d00c4f5 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.41.11 + 2.41.14 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 7472f5b884..0ffde63a7c 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -110,7 +110,7 @@ io.avaje avaje-inject - 12.2 + 12.3 test @@ -167,7 +167,7 @@ org.assertj assertj-core - 3.27.6 + 3.27.7 test diff --git a/pom.xml b/pom.xml index 861a31dcb9..f5621a54f9 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ 7.0.0 - 1.5.25 + 1.5.26 2.25.3 2.0.17 @@ -96,7 +96,7 @@ 1.6.0 - 4.2.37 + 4.2.38 2.3.0 @@ -119,8 +119,8 @@ 2.5.2 - 12.2 - 3.9 + 12.3 + 3.10 2.16 @@ -151,7 +151,7 @@ 0.8.14 6.0.2 6.0.0 - 3.27.6 + 3.27.7 5.21.0 ${user.home}${file.separator}.m2${file.separator}repository org${file.separator}mockito${file.separator}mockito-core${file.separator}${mockito.version}${file.separator}mockito-core-${mockito.version}.jar @@ -1519,7 +1519,7 @@ com.diffplug.spotless spotless-maven-plugin - 3.1.0 + 3.2.0 true From 33de401e878c25620a5d5100c7848383d4abc92d Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Sat, 31 Jan 2026 17:27:39 +0200 Subject: [PATCH 08/31] add missing form() with default value --- jooby/src/main/java/io/jooby/Context.java | 13 +++++++++++++ jooby/src/main/java/io/jooby/DefaultContext.java | 5 +++++ jooby/src/main/java/io/jooby/ForwardingContext.java | 5 +++++ .../jooby-apt/src/test/java/tests/i3761/C3761.java | 6 ++++++ .../src/test/java/tests/i3761/C3761Jakarta.java | 6 ++++++ .../src/test/java/tests/i3761/Issue3761.java | 2 ++ 6 files changed, 37 insertions(+) diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 1f5be9f676..5017687243 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -817,6 +817,19 @@ default Locale locale() { */ Value form(@NonNull String name); + /** + * Get a form field that matches the given name. + * + *

File upload retrieval is available using {@link Context#file(String)}. + * + *

Only for multipart/form-data request. + * + * @param name Field name. + * @param defaultValue Default value. + * @return Multipart value. + */ + Value form(@NonNull String name, @NonNull String defaultValue); + /** * Convert form data to the given type. * diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 40edf0cb25..eec97ffd45 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -452,6 +452,11 @@ default Value form(@NonNull String name) { return form().get(name); } + @Override + default Value form(@NonNull String name, @NonNull String defaultValue) { + return form().getOrDefault(name, defaultValue); + } + @Override default T form(@NonNull Class type) { return form().to(type); diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 5d2a8fcfe6..f271c61d11 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -1001,6 +1001,11 @@ public Value form(@NonNull String name) { return ctx.form(name); } + @Override + public Value form(@NonNull String name, @NonNull String defaultValue) { + return ctx.form(name, defaultValue); + } + @Override public T form(@NonNull Class type) { return ctx.form(type); diff --git a/modules/jooby-apt/src/test/java/tests/i3761/C3761.java b/modules/jooby-apt/src/test/java/tests/i3761/C3761.java index ddca9bdaa3..578c5ee07d 100644 --- a/modules/jooby-apt/src/test/java/tests/i3761/C3761.java +++ b/modules/jooby-apt/src/test/java/tests/i3761/C3761.java @@ -5,6 +5,7 @@ */ package tests.i3761; +import io.jooby.annotation.FormParam; import io.jooby.annotation.GET; import io.jooby.annotation.Path; import io.jooby.annotation.QueryParam; @@ -31,4 +32,9 @@ public String emptySet(@QueryParam("") String emptySet) { public String string(@QueryParam("Hello") String stringVal) { return stringVal; } + + @GET("/boolVal") + public boolean bool(@FormParam("false") boolean boolVal) { + return boolVal; + } } diff --git a/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java b/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java index 4802038a84..aadae957bd 100644 --- a/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java +++ b/modules/jooby-apt/src/test/java/tests/i3761/C3761Jakarta.java @@ -9,6 +9,7 @@ import io.jooby.annotation.Path; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.FormParam; @Path("/3761") public class C3761Jakarta { @@ -32,4 +33,9 @@ public String emptySet(@QueryParam("emptySet") @DefaultValue("") String emptySet public String string(@QueryParam("stringVal") @DefaultValue("Hello") String stringVal) { return stringVal; } + + @GET("/boolVal") + public boolean bool(@FormParam("boolVal") @DefaultValue("false") boolean boolVal) { + return boolVal; + } } diff --git a/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java b/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java index 6a18f83c1b..963f350c74 100644 --- a/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java +++ b/modules/jooby-apt/src/test/java/tests/i3761/Issue3761.java @@ -30,5 +30,7 @@ private static void assertSourceCodeRespectDefaultValues(String source) { source.contains("return c.emptySet(ctx.query(\"emptySet\", \"\").value());")); assertTrue( source.contains("return c.string(ctx.query(\"stringVal\", \"Hello\").value());")); + assertTrue( + source.contains("return c.bool(ctx.form(\"boolVal\", \"false\").booleanValue());")); } } From 1dc2518e831b8b821088ccd744e335051f9fae71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:20:32 +0000 Subject: [PATCH 09/31] build(deps): bump the dependencies group across 1 directory with 42 updates Bumps the dependencies group with 42 updates in the / directory: | Package | From | To | | --- | --- | --- | | [io.netty:netty-bom](https://github.com/netty/netty) | `4.2.9.Final` | `4.2.10.Final` | | [io.netty:netty-codec-http2](https://github.com/netty/netty) | `4.2.9.Final` | `4.2.10.Final` | | [io.netty:netty-transport-native-epoll](https://github.com/netty/netty) | `4.2.9.Final` | `4.2.10.Final` | | [io.netty:netty-transport-native-kqueue](https://github.com/netty/netty) | `4.2.9.Final` | `4.2.10.Final` | | [io.netty:netty-transport-native-io_uring](https://github.com/netty/netty) | `4.2.9.Final` | `4.2.10.Final` | | [org.junit:junit-bom](https://github.com/junit-team/junit-framework) | `6.0.2` | `6.0.3` | | [io.avaje:avaje-inject](https://github.com/avaje/avaje-inject) | `12.3` | `12.4` | | io.avaje:avaje-inject-generator | `12.3` | `12.4` | | [io.avaje:avaje-jsonb](https://github.com/avaje/avaje-jsonb) | `3.10` | `3.11` | | io.avaje:avaje-jsonb-generator | `3.10` | `3.11` | | [io.pebbletemplates:pebble](https://github.com/PebbleTemplates/pebble) | `4.1.0` | `4.1.1` | | [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) | `1.5.26` | `1.5.31` | | [io.ebean:ebean](https://github.com/ebean-orm/ebean) | `17.2.0` | `17.2.1` | | [io.ebean:ebean-querybean](https://github.com/ebean-orm/ebean) | `17.2.0` | `17.2.1` | | [io.ebean:querybean-generator](https://github.com/ebean-orm/ebean) | `17.2.0` | `17.2.1` | | [io.ebean:ebean-test](https://github.com/ebean-orm/ebean) | `17.2.0` | `17.2.1` | | [io.undertow:undertow-core](https://github.com/undertow-io/undertow) | `2.3.22.Final` | `2.3.23.Final` | | org.eclipse.jetty:jetty-server | `12.1.5` | `12.1.6` | | org.eclipse.jetty.websocket:jetty-websocket-core-server | `12.1.5` | `12.1.6` | | org.eclipse.jetty.websocket:jetty-websocket-jetty-api | `12.1.5` | `12.1.6` | | org.eclipse.jetty.websocket:jetty-websocket-jetty-server | `12.1.5` | `12.1.6` | | org.eclipse.jetty.http2:jetty-http2-server | `12.1.5` | `12.1.6` | | org.eclipse.jetty:jetty-alpn-java-server | `12.1.5` | `12.1.6` | | org.eclipse.jetty.http2:jetty-http2-client | `12.1.5` | `12.1.6` | | [com.bucket4j:bucket4j_jdk17-core](https://github.com/bucket4j/bucket4j) | `8.16.0` | `8.16.1` | | [org.jetbrains.kotlin:kotlin-stdlib](https://github.com/JetBrains/kotlin) | `2.3.0` | `2.3.10` | | [org.jetbrains.kotlin:kotlin-reflect](https://github.com/JetBrains/kotlin) | `2.3.0` | `2.3.10` | | org.jetbrains.kotlin:kotlin-maven-plugin | `2.3.0` | `2.3.10` | | [io.lettuce:lettuce-core](https://github.com/redis/lettuce) | `7.2.1.RELEASE` | `7.4.0.RELEASE` | | [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) | `3.14.1` | `3.15.0` | | [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) | `3.2.0` | `3.2.1` | | [org.jboss.logging:jboss-logging](https://github.com/jboss-logging/jboss-logging) | `3.6.1.Final` | `3.6.2.Final` | | [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) | `13.0.0` | `13.2.0` | | [net.datafaker:datafaker](https://github.com/datafaker-net/datafaker) | `2.5.3` | `2.5.4` | | [commons-codec:commons-codec](https://github.com/apache/commons-codec) | `1.20.0` | `1.21.0` | | [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) | `1.18.4` | `1.18.5` | | [gg.jte:jte](https://github.com/casid/jte) | `3.2.2` | `3.2.3` | | [gg.jte:jte-models](https://github.com/casid/jte) | `3.2.2` | `3.2.3` | | [com.github.kagkarlsson:db-scheduler](https://github.com/kagkarlsson/db-scheduler) | `16.7.0` | `16.7.1` | | software.amazon.awssdk:bom | `2.41.14` | `2.41.29` | | [io.projectreactor:reactor-core](https://github.com/reactor/reactor-core) | `3.8.2` | `3.8.3` | | [org.asynchttpclient:async-http-client](https://github.com/AsyncHttpClient/async-http-client) | `3.0.6` | `3.0.7` | Updates `io.netty:netty-bom` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-codec-http2` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport-native-epoll` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport-native-kqueue` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport-native-io_uring` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `org.junit:junit-bom` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](https://github.com/junit-team/junit-framework/compare/r6.0.2...r6.0.3) Updates `io.avaje:avaje-inject` from 12.3 to 12.4 - [Release notes](https://github.com/avaje/avaje-inject/releases) - [Commits](https://github.com/avaje/avaje-inject/compare/12.3...12.4) Updates `io.avaje:avaje-inject-generator` from 12.3 to 12.4 Updates `io.avaje:avaje-inject-generator` from 12.3 to 12.4 Updates `io.avaje:avaje-jsonb` from 3.10 to 3.11 - [Release notes](https://github.com/avaje/avaje-jsonb/releases) - [Commits](https://github.com/avaje/avaje-jsonb/compare/3.10...3.11) Updates `io.avaje:avaje-jsonb-generator` from 3.10 to 3.11 Updates `io.avaje:avaje-jsonb-generator` from 3.10 to 3.11 Updates `io.pebbletemplates:pebble` from 4.1.0 to 4.1.1 - [Release notes](https://github.com/PebbleTemplates/pebble/releases) - [Commits](https://github.com/PebbleTemplates/pebble/compare/4.1.0...4.1.1) Updates `ch.qos.logback:logback-classic` from 1.5.26 to 1.5.31 - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.26...v_1.5.31) Updates `io.ebean:ebean` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-querybean` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:querybean-generator` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-test` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-querybean` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:querybean-generator` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-test` from 17.2.0 to 17.2.1 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.undertow:undertow-core` from 2.3.22.Final to 2.3.23.Final - [Release notes](https://github.com/undertow-io/undertow/releases) - [Commits](https://github.com/undertow-io/undertow/compare/2.3.22.Final...2.3.23.Final) Updates `org.eclipse.jetty:jetty-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.websocket:jetty-websocket-core-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-api` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.http2:jetty-http2-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty:jetty-alpn-java-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.http2:jetty-http2-client` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.websocket:jetty-websocket-core-server` from 12.1.5 to 12.1.6 Updates `com.bucket4j:bucket4j_jdk17-core` from 8.16.0 to 8.16.1 - [Release notes](https://github.com/bucket4j/bucket4j/releases) - [Commits](https://github.com/bucket4j/bucket4j/compare/8.16.0...8.16.1) Updates `org.jetbrains.kotlin:kotlin-stdlib` from 2.3.0 to 2.3.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.0...v2.3.10) Updates `org.jetbrains.kotlin:kotlin-reflect` from 2.3.0 to 2.3.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.0...v2.3.10) Updates `org.jetbrains.kotlin:kotlin-maven-plugin` from 2.3.0 to 2.3.10 Updates `org.jetbrains.kotlin:kotlin-reflect` from 2.3.0 to 2.3.10 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.3.0...v2.3.10) Updates `io.lettuce:lettuce-core` from 7.2.1.RELEASE to 7.4.0.RELEASE - [Release notes](https://github.com/redis/lettuce/releases) - [Changelog](https://github.com/redis/lettuce/blob/main/RELEASE-NOTES.md) - [Commits](https://github.com/redis/lettuce/compare/7.2.1.RELEASE...7.4.0.RELEASE) Updates `org.jetbrains.kotlin:kotlin-maven-plugin` from 2.3.0 to 2.3.10 Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.14.1 to 3.15.0 - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.14.1...maven-compiler-plugin-3.15.0) Updates `com.diffplug.spotless:spotless-maven-plugin` from 3.2.0 to 3.2.1 - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](https://github.com/diffplug/spotless/compare/lib/3.2.0...maven/3.2.1) Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-api` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.websocket:jetty-websocket-jetty-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty.http2:jetty-http2-server` from 12.1.5 to 12.1.6 Updates `org.eclipse.jetty:jetty-alpn-java-server` from 12.1.5 to 12.1.6 Updates `io.netty:netty-codec-http2` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport-native-epoll` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport-native-kqueue` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport-native-io_uring` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `org.jboss.logging:jboss-logging` from 3.6.1.Final to 3.6.2.Final - [Release notes](https://github.com/jboss-logging/jboss-logging/releases) - [Commits](https://github.com/jboss-logging/jboss-logging/compare/3.6.1.Final...v3.6.2.Final) Updates `com.puppycrawl.tools:checkstyle` from 13.0.0 to 13.2.0 - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-13.0.0...checkstyle-13.2.0) Updates `net.datafaker:datafaker` from 2.5.3 to 2.5.4 - [Release notes](https://github.com/datafaker-net/datafaker/releases) - [Changelog](https://github.com/datafaker-net/datafaker/blob/main/RELEASE_PROCESS.md) - [Commits](https://github.com/datafaker-net/datafaker/compare/2.5.3...2.5.4) Updates `commons-codec:commons-codec` from 1.20.0 to 1.21.0 - [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.20.0...rel/commons-codec-1.21.0) Updates `net.bytebuddy:byte-buddy` from 1.18.4 to 1.18.5 - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.4...byte-buddy-1.18.5) Updates `gg.jte:jte` from 3.2.2 to 3.2.3 - [Release notes](https://github.com/casid/jte/releases) - [Commits](https://github.com/casid/jte/compare/3.2.2...3.2.3) Updates `gg.jte:jte-models` from 3.2.2 to 3.2.3 - [Release notes](https://github.com/casid/jte/releases) - [Commits](https://github.com/casid/jte/compare/3.2.2...3.2.3) Updates `gg.jte:jte-models` from 3.2.2 to 3.2.3 - [Release notes](https://github.com/casid/jte/releases) - [Commits](https://github.com/casid/jte/compare/3.2.2...3.2.3) Updates `com.github.kagkarlsson:db-scheduler` from 16.7.0 to 16.7.1 - [Release notes](https://github.com/kagkarlsson/db-scheduler/releases) - [Commits](https://github.com/kagkarlsson/db-scheduler/compare/v16.7.0...v16.7.1) Updates `software.amazon.awssdk:bom` from 2.41.14 to 2.41.29 Updates `io.projectreactor:reactor-core` from 3.8.2 to 3.8.3 - [Release notes](https://github.com/reactor/reactor-core/releases) - [Commits](https://github.com/reactor/reactor-core/compare/v3.8.2...v3.8.3) Updates `org.asynchttpclient:async-http-client` from 3.0.6 to 3.0.7 - [Release notes](https://github.com/AsyncHttpClient/async-http-client/releases) - [Changelog](https://github.com/AsyncHttpClient/async-http-client/blob/main/CHANGES.md) - [Commits](https://github.com/AsyncHttpClient/async-http-client/compare/async-http-client-project-3.0.6...async-http-client-project-3.0.7) Updates `org.eclipse.jetty.http2:jetty-http2-client` from 12.1.5 to 12.1.6 --- updated-dependencies: - dependency-name: io.netty:netty-bom dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-epoll dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-kqueue dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-io_uring dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.junit:junit-bom dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.avaje:avaje-inject dependency-version: '12.4' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.4' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.4' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb dependency-version: '3.11' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.11' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.11' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.pebbletemplates:pebble dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.5.31 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean-querybean dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:querybean-generator dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean-test dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean-querybean dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:querybean-generator dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean-test dependency-version: 17.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.undertow:undertow-core dependency-version: 2.3.23.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty:jetty-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-core-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-api dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty:jetty-alpn-java-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-client dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-core-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.bucket4j:bucket4j_jdk17-core dependency-version: 8.16.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jetbrains.kotlin:kotlin-stdlib dependency-version: 2.3.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-version: 2.3.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jetbrains.kotlin:kotlin-maven-plugin dependency-version: 2.3.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-version: 2.3.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.lettuce:lettuce-core dependency-version: 7.4.0.RELEASE dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.jetbrains.kotlin:kotlin-maven-plugin dependency-version: 2.3.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.15.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: com.diffplug.spotless:spotless-maven-plugin dependency-version: 3.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-api dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.websocket:jetty-websocket-jetty-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty:jetty-alpn-java-server dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-codec-http2 dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-epoll dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-kqueue dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.netty:netty-transport-native-io_uring dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.jboss.logging:jboss-logging dependency-version: 3.6.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.puppycrawl.tools:checkstyle dependency-version: 13.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: net.datafaker:datafaker dependency-version: 2.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: commons-codec:commons-codec dependency-version: 1.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: gg.jte:jte dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: gg.jte:jte-models dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: gg.jte:jte-models dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.github.kagkarlsson:db-scheduler dependency-version: 16.7.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.41.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.projectreactor:reactor-core dependency-version: 3.8.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.asynchttpclient:async-http-client dependency-version: 3.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.eclipse.jetty.http2:jetty-http2-client dependency-version: 12.1.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 10 +++++----- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- pom.xml | 30 +++++++++++++++--------------- tests/pom.xml | 4 ++-- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index a66d00c4f5..4a7cddb135 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.41.14 + 2.41.29 diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index 032032caf8..ea06643c1a 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -22,7 +22,7 @@ com.github.kagkarlsson db-scheduler - 16.7.0 + 16.7.1 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 0ffde63a7c..13f2837b88 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -53,7 +53,7 @@ com.puppycrawl.tools checkstyle - 13.0.0 + 13.2.0 @@ -69,12 +69,12 @@ net.datafaker datafaker - 2.5.3 + 2.5.4 commons-codec commons-codec - 1.20.0 + 1.21.0 @@ -110,7 +110,7 @@ io.avaje avaje-inject - 12.3 + 12.4 test @@ -156,7 +156,7 @@ net.bytebuddy byte-buddy - 1.18.4 + 1.18.5 test diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index bfe220da23..5c4dfba1f9 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -26,7 +26,7 @@ io.projectreactor reactor-core - 3.8.2 + 3.8.3 diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index 32d5c2d1ec..619ed5486f 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -28,7 +28,7 @@ org.jboss.logging jboss-logging - 3.6.1.Final + 3.6.2.Final diff --git a/pom.xml b/pom.xml index f5621a54f9..3c64919c20 100644 --- a/pom.xml +++ b/pom.xml @@ -60,24 +60,24 @@ 2.3.34 4.5.0 1.3.7 - 4.1.0 + 4.1.1 2.21.0 2.13.2 3.0.1 3.0.4 2.4.0 3.1.3.RELEASE - 3.2.2 + 3.2.3 7.0.2 1.2 7.0.4.Final - 17.2.0 + 17.2.1 3.51.0 11.20.1 25.0 - 7.2.1.RELEASE + 7.4.0.RELEASE 2.13.1 4.1.1 3.2.3 @@ -88,7 +88,7 @@ 7.0.0 - 1.5.26 + 1.5.31 2.25.3 2.0.17 @@ -105,9 +105,9 @@ 9.9.1 - 2.3.22.Final - 12.1.5 - 4.2.9.Final + 2.3.23.Final + 12.1.6 + 4.2.10.Final 5.0.7 @@ -119,8 +119,8 @@ 2.5.2 - 12.3 - 3.10 + 12.4 + 3.11 2.16 @@ -135,7 +135,7 @@ 6.3.1 2.5.2 9.2.1 - 8.16.0 + 8.16.1 1.12.797 4.17.0 1.9.3 @@ -143,13 +143,13 @@ 2.21.0 - 2.3.0 + 2.3.10 1.10.2 true 0.8.14 - 6.0.2 + 6.0.3 6.0.0 3.27.7 5.21.0 @@ -172,7 +172,7 @@ 3.2.0 3.8.0 2.43 - 3.14.1 + 3.15.0 3.9.12 3.6.2 3.2.8 @@ -1519,7 +1519,7 @@ com.diffplug.spotless spotless-maven-plugin - 3.2.0 + 3.2.1 true diff --git a/tests/pom.xml b/tests/pom.xml index 5c2189d625..14242d8834 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -238,13 +238,13 @@ org.asynchttpclient async-http-client - 3.0.6 + 3.0.7 commons-codec commons-codec - 1.20.0 + 1.21.0 From 070086752602f9d5262ccef65f21718cf85e11f7 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 25 Jan 2026 11:09:02 -0300 Subject: [PATCH 10/31] netty: redo pipeline for HTTP2 --- .../jooby/internal/netty/Http2Extension.java | 66 ------- .../jooby/internal/netty/Http2Settings.java | 24 --- .../io/jooby/internal/netty/NettyContext.java | 38 ++-- .../jooby/internal/netty/NettyPipeline.java | 177 ++++++++++++------ .../internal/netty/NettyRequestDecoder.java | 52 ----- .../internal/netty/NettyResponseEncoder.java | 21 --- .../internal/netty/NettyServerCodec.java | 136 ++++++++++++++ .../netty/http2/Http2OrHttp11Handler.java | 36 ---- .../http2/Http2PrefaceOrHttpHandler.java | 44 ----- .../netty/http2/NettyHttp2Configurer.java | 62 ------ .../test/java/io/jooby/test/FeaturedTest.java | 2 +- .../test/java/io/jooby/test/Http2Test.java | 61 +++++- 12 files changed, 333 insertions(+), 386 deletions(-) delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Extension.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Settings.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerCodec.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2OrHttp11Handler.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2PrefaceOrHttpHandler.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/NettyHttp2Configurer.java diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Extension.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Extension.java deleted file mode 100644 index 1ff0bb2d4d..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Extension.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import io.netty.channel.ChannelOutboundHandler; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.http.HttpServerUpgradeHandler; - -public class Http2Extension { - - private Http2Settings settings; - - private Consumer http11; - - private BiConsumer> - http11Upgrade; - - private BiConsumer> http2; - - private BiConsumer> http2c; - - public Http2Extension( - Http2Settings settings, - Consumer http11, - BiConsumer> http11Upgrade, - BiConsumer> http2, - BiConsumer> http2c) { - this.settings = settings; - this.http11 = http11; - this.http11Upgrade = http11Upgrade; - this.http2 = http2; - this.http2c = http2c; - } - - public boolean isSecure() { - return settings.isSecure(); - } - - public void http11(ChannelPipeline pipeline) { - this.http11.accept(pipeline); - } - - public void http2( - ChannelPipeline pipeline, Function factory) { - this.http2.accept(pipeline, () -> factory.apply(settings)); - } - - public void http2c( - ChannelPipeline pipeline, Function factory) { - this.http2c.accept(pipeline, () -> factory.apply(settings)); - } - - public void http11Upgrade( - ChannelPipeline pipeline, - Function factory) { - this.http11Upgrade.accept(pipeline, () -> factory.apply(settings)); - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Settings.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Settings.java deleted file mode 100644 index 4a99f63d5d..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/Http2Settings.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -public class Http2Settings { - private final int maxRequestSize; - private final boolean secure; - - public Http2Settings(long maxRequestSize, boolean secure) { - this.maxRequestSize = (int) maxRequestSize; - this.secure = secure; - } - - public boolean isSecure() { - return secure; - } - - public int getMaxRequestSize() { - return maxRequestSize; - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java index 0f1c34d959..91d2496e92 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java @@ -46,12 +46,7 @@ import io.jooby.value.Value; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.ChannelPromise; -import io.netty.channel.DefaultFileRegion; +import io.netty.channel.*; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import io.netty.handler.codec.http.multipart.*; @@ -320,7 +315,7 @@ public String getProtocol() { @NonNull @Override public List getClientCertificates() { - SslHandler sslHandler = (SslHandler) ctx.channel().pipeline().get("ssl"); + var sslHandler = ssl(); if (sslHandler != null) { try { return List.of(sslHandler.engine().getSession().getPeerCertificates()); @@ -334,11 +329,22 @@ public List getClientCertificates() { @NonNull @Override public String getScheme() { if (scheme == null) { - scheme = ctx.pipeline().get("ssl") == null ? "http" : "https"; + scheme = ssl() == null ? "http" : "https"; } return scheme; } + private SslHandler ssl() { + return (SslHandler) + Stream.of(ctx.channel(), ctx.channel().parent()) + .filter(Objects::nonNull) + .map(Channel::pipeline) + .map(it -> it.get("ssl")) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + @NonNull @Override public Context setScheme(@NonNull String scheme) { this.scheme = scheme; @@ -416,7 +422,7 @@ public Context upgrade(WebSocket.Initializer handler) { ? conf.getBytes("websocket.maxSize").intValue() : WebSocket.MAX_BUFFER_SIZE; String webSocketURL = getProtocol() + "://" + req.headers().get(HttpHeaderNames.HOST) + path; - WebSocketDecoderConfig config = + var config = WebSocketDecoderConfig.newBuilder() .allowExtensions(true) .allowMaskMismatch(false) @@ -425,7 +431,7 @@ public Context upgrade(WebSocket.Initializer handler) { .build(); webSocket = new NettyWebSocket(this); handler.init(Context.readOnly(this), webSocket); - FullHttpRequest webSocketRequest = + var webSocketRequest = new DefaultFullHttpRequest( HTTP_1_1, req.method(), @@ -433,6 +439,8 @@ public Context upgrade(WebSocket.Initializer handler) { Unpooled.EMPTY_BUFFER, req.headers(), EmptyHttpHeaders.INSTANCE); + var codec = ctx.pipeline().get(NettyServerCodec.class); + codec.webSocketHandshake(ctx); WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(webSocketURL, null, config); WebSocketServerHandshaker handshaker = factory.newHandshaker(webSocketRequest); @@ -856,15 +864,9 @@ private long responseLength() { private void prepareChunked() { responseStarted = true; // remove flusher, doesn't play well with streaming/chunked responses - ChannelPipeline pipeline = ctx.pipeline(); + var pipeline = ctx.pipeline(); if (pipeline.get("chunker") == null) { - String base = - Stream.of("compressor", "encoder", "codec", "http2") - .filter(name -> pipeline.get(name) != null) - .findFirst() - .orElseThrow( - () -> new IllegalStateException("No available handler for chunk writer")); - pipeline.addAfter(base, "chunker", new ChunkedWriteHandler()); + pipeline.addBefore("handler", "chunker", new ChunkedWriteHandler()); } if (!setHeaders.contains(CONTENT_LENGTH)) { setHeaders.set(TRANSFER_ENCODING, CHUNKED); diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java index 1fd488d2be..61437319a0 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java @@ -5,20 +5,23 @@ */ package io.jooby.internal.netty; +import java.util.List; import java.util.concurrent.ScheduledExecutorService; -import java.util.function.Supplier; import io.jooby.Context; -import io.jooby.internal.netty.http2.NettyHttp2Configurer; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelOutboundHandler; -import io.netty.channel.ChannelPipeline; +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http2.*; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.SslContext; public class NettyPipeline extends ChannelInitializer { private static final String H2_HANDSHAKE = "h2-handshake"; + private final SslContext sslContext; private final HttpDecoderConfig decoderConfig; private final Context.Selector contextSelector; @@ -53,45 +56,61 @@ public NettyPipeline( this.compressionLevel = compressionLevel; } - private NettyHandler createHandler(ScheduledExecutorService executor) { - return new NettyHandler( - new NettyDateService(executor), - contextSelector, - maxRequestSize, - maxFormFields, - bufferSize, - defaultHeaders, - http2); - } - @Override public void initChannel(SocketChannel ch) { - var p = ch.pipeline(); + ChannelPipeline p = ch.pipeline(); + if (sslContext != null) { p.addLast("ssl", sslContext.newHandler(ch.alloc())); } - // https://github.com/jooby-project/jooby/issues/3433: - // using new FlushConsolidationHandler(DEFAULT_EXPLICIT_FLUSH_AFTER_FLUSHES, true) - // cause the bug, for now I'm going to remove flush consolidating handler... doesn't seem to - // help much - // p.addLast(new FlushConsolidationHandler(DEFAULT_EXPLICIT_FLUSH_AFTER_FLUSHES, false)); + if (http2) { - var settings = new Http2Settings(maxRequestSize, sslContext != null); - var extension = - new Http2Extension( - settings, this::http11, this::http11Upgrade, this::http2, this::http2c); - var configurer = new NettyHttp2Configurer(); - var handshake = configurer.configure(extension); - - p.addLast(H2_HANDSHAKE, handshake); - additionalHandlers(p); - p.addLast("handler", createHandler(ch.eventLoop())); + p.addLast(H2_HANDSHAKE, setupHttp2Handshake(sslContext != null)); } else { - http11(p); + setupHttp11(p); + } + } + + private void setupHttp11(ChannelPipeline p) { + p.addLast("codec", createServerCodec()); + addCommonHandlers(p); + p.addLast("handler", createHandler(p.channel().eventLoop())); + } + + private void setupHttp2(ChannelPipeline pipeline) { + var frameCodec = + Http2FrameCodecBuilder.forServer() + .initialSettings(Http2Settings.defaultSettings().maxFrameSize((int) maxRequestSize)) + .build(); + + pipeline.addLast("http2-codec", frameCodec); + pipeline.addLast( + "http2-multiplex", new Http2MultiplexHandler(new Http2StreamInitializer(this))); + } + + private void setupHttp11Upgrade(ChannelPipeline pipeline) { + var serverCodec = createServerCodec(); + pipeline.addLast("codec", serverCodec); + + pipeline.addLast( + "h2upgrade", + new HttpServerUpgradeHandler( + serverCodec, + protocol -> "h2c".equals(protocol.toString()) ? createH2CUpgradeCodec() : null, + (int) maxRequestSize)); + + addCommonHandlers(pipeline); + pipeline.addLast("handler", createHandler(pipeline.channel().eventLoop())); + } + + private ChannelInboundHandler setupHttp2Handshake(boolean secure) { + if (secure) { + return new AlpnHandler(this); } + return new Http2PrefaceOrHttpHandler(this); } - private void additionalHandlers(ChannelPipeline p) { + private void addCommonHandlers(ChannelPipeline p) { if (expectContinue) { p.addLast("expect-continue", new HttpServerExpectContinueHandler()); } @@ -101,32 +120,80 @@ private void additionalHandlers(ChannelPipeline p) { } } - private void http2(ChannelPipeline pipeline, Supplier factory) { - pipeline.addAfter(H2_HANDSHAKE, "http2", factory.get()); + private Http2ServerUpgradeCodec createH2CUpgradeCodec() { + return new Http2ServerUpgradeCodec( + Http2FrameCodecBuilder.forServer().build(), + new Http2MultiplexHandler(new Http2StreamInitializer(this))); } - private void http2c(ChannelPipeline pipeline, Supplier factory) { - pipeline.addAfter(H2_HANDSHAKE, "http2", factory.get()); + private NettyHandler createHandler(ScheduledExecutorService executor) { + return new NettyHandler( + new NettyDateService(executor), + contextSelector, + maxRequestSize, + maxFormFields, + bufferSize, + defaultHeaders, + http2); } - private void http11Upgrade( - ChannelPipeline pipeline, Supplier factory) { - // direct http1 to h2c - HttpServerCodec serverCodec = new HttpServerCodec(decoderConfig); - pipeline.addAfter(H2_HANDSHAKE, "codec", serverCodec); - pipeline.addAfter( - "codec", - "h2upgrade", - new HttpServerUpgradeHandler( - serverCodec, - protocol -> protocol.toString().equals("h2c") ? factory.get() : null, - (int) maxRequestSize)); + private NettyServerCodec createServerCodec() { + return new NettyServerCodec(decoderConfig); } - private void http11(ChannelPipeline p) { - p.addLast("decoder", new NettyRequestDecoder(decoderConfig)); - p.addLast("encoder", new NettyResponseEncoder()); - additionalHandlers(p); - p.addLast("handler", createHandler(p.channel().eventLoop())); + /** Handles the transition from ALPN to H1 or H2 */ + private static class AlpnHandler extends ApplicationProtocolNegotiationHandler { + private final NettyPipeline pipeline; + + AlpnHandler(NettyPipeline pipeline) { + super(ApplicationProtocolNames.HTTP_1_1); + this.pipeline = pipeline; + } + + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + pipeline.setupHttp2(ctx.pipeline()); + } else { + pipeline.setupHttp11(ctx.pipeline()); + } + } + } + + /** Detects HTTP/2 connection preface or upgrades to H1/H2C */ + private static class Http2PrefaceOrHttpHandler extends ByteToMessageDecoder { + private static final int PRI = 0x50524920; // "PRI " + private final NettyPipeline pipeline; + + Http2PrefaceOrHttpHandler(NettyPipeline pipeline) { + this.pipeline = pipeline; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + if (in.readableBytes() < 4) return; + + if (in.getInt(in.readerIndex()) == PRI) { + pipeline.setupHttp2(ctx.pipeline()); + } else { + pipeline.setupHttp11Upgrade(ctx.pipeline()); + } + ctx.pipeline().remove(this); + } + } + + /** Initializes the child channels created for each HTTP/2 stream */ + private static class Http2StreamInitializer extends ChannelInitializer { + private final NettyPipeline pipeline; + + Http2StreamInitializer(NettyPipeline pipeline) { + this.pipeline = pipeline; + } + + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast("http2", new Http2StreamFrameToHttpObjectCodec(true)); + ch.pipeline().addLast("handler", pipeline.createHandler(ch.eventLoop())); + } } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java deleted file mode 100644 index 71788e2c3a..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -import io.netty.handler.codec.http.*; - -public class NettyRequestDecoder extends HttpRequestDecoder { - - private static final String GET = HttpMethod.GET.name(); - private static final String POST = HttpMethod.POST.name(); - private static final String PUT = HttpMethod.PUT.name(); - private static final String DELETE = HttpMethod.DELETE.name(); - - public NettyRequestDecoder(HttpDecoderConfig config) { - super(config); - } - - @Override - protected HttpMessage createMessage(String[] initialLine) throws Exception { - return new DefaultHttpRequest( - HttpVersion.valueOf(initialLine[2]), - valueOf(initialLine[0]), - initialLine[1], - headersFactory); - } - - @Override - protected boolean isContentAlwaysEmpty(HttpMessage msg) { - return false; - } - - private static HttpMethod valueOf(String name) { - // fast-path - if (name == GET) { - return HttpMethod.GET; - } - if (name == POST) { - return HttpMethod.POST; - } - if (name == DELETE) { - return HttpMethod.DELETE; - } - if (name == PUT) { - return HttpMethod.PUT; - } - // "slow"-path: ensure method is on upper case - return HttpMethod.valueOf(name.toUpperCase()); - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java deleted file mode 100644 index c8c26cb2b1..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpResponseEncoder; - -public class NettyResponseEncoder extends HttpResponseEncoder { - @Override - protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) { - if (headers.getClass() == HeadersMultiMap.class) { - ((HeadersMultiMap) headers).encode(buf); - } else { - super.encodeHeaders(headers, buf); - } - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerCodec.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerCodec.java new file mode 100644 index 0000000000..25544286dd --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerCodec.java @@ -0,0 +1,136 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Queue; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.CombinedChannelDuplexHandler; +import io.netty.handler.codec.http.*; + +/** + * Copy of {@link HttpServerCodec} with a custom request method parser and optimized header response + * writer. + */ +public class NettyServerCodec + extends CombinedChannelDuplexHandler + implements HttpServerUpgradeHandler.SourceCodec { + + /** A queue that is used for correlating a request and a response. */ + private final Queue queue = new ArrayDeque(); + + private final HttpDecoderConfig decoderConfig; + + /** Creates a new instance with the specified decoder configuration. */ + public NettyServerCodec(HttpDecoderConfig decoderConfig) { + this.decoderConfig = decoderConfig; + init(new HttpServerRequestDecoder(decoderConfig), new HttpServerResponseEncoder()); + } + + /** + * Web socket looks for these two component while doing the upgrade. + * + * @param ctx Channel context. + */ + /*package*/ void webSocketHandshake(ChannelHandlerContext ctx) { + var p = ctx.pipeline(); + var codec = p.context(getClass()).name(); + p.addBefore(codec, "encoder", new HttpServerResponseEncoder()); + p.addBefore(codec, "decoder", new HttpServerRequestDecoder(decoderConfig)); + p.remove(this); + } + + /** + * Upgrades to another protocol from HTTP. Removes the {@link HttpRequestDecoder} and {@link + * HttpResponseEncoder} from the pipeline. + */ + @Override + public void upgradeFrom(ChannelHandlerContext ctx) { + ctx.pipeline().remove(this); + } + + private final class HttpServerRequestDecoder extends HttpRequestDecoder { + HttpServerRequestDecoder(HttpDecoderConfig config) { + super(config); + } + + @Override + protected HttpMessage createMessage(String[] initialLine) { + return new DefaultHttpRequest( + // Do strict version checking + HttpVersion.valueOf(initialLine[2]), + httpMethod(initialLine[0]), + initialLine[1], + headersFactory); + } + + public static HttpMethod httpMethod(String name) { + return switch (name) { + case "OPTIONS" -> HttpMethod.OPTIONS; + case "GET" -> HttpMethod.GET; + case "HEAD" -> HttpMethod.HEAD; + case "POST" -> HttpMethod.POST; + case "PUT" -> HttpMethod.PUT; + case "PATCH" -> HttpMethod.PATCH; + case "DELETE" -> HttpMethod.DELETE; + case "TRACE" -> HttpMethod.TRACE; + case "CONNECT" -> HttpMethod.CONNECT; + default -> new HttpMethod(name.toUpperCase()); + }; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List out) + throws Exception { + int oldSize = out.size(); + super.decode(ctx, buffer, out); + int size = out.size(); + for (int i = oldSize; i < size; i++) { + Object obj = out.get(i); + if (obj instanceof HttpRequest) { + queue.add(((HttpRequest) obj).method()); + } + } + } + } + + private final class HttpServerResponseEncoder extends HttpResponseEncoder { + + private HttpMethod method; + + @Override + protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) { + if (!isAlwaysEmpty + && HttpMethod.CONNECT.equals(method) + && msg.status().codeClass() == HttpStatusClass.SUCCESS) { + // Stripping Transfer-Encoding: + // See https://tools.ietf.org/html/rfc7230#section-3.3.1 + msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING); + return; + } + + super.sanitizeHeadersBeforeEncode(msg, isAlwaysEmpty); + } + + @Override + protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) { + if (headers.getClass() == HeadersMultiMap.class) { + ((HeadersMultiMap) headers).encode(buf); + } else { + super.encodeHeaders(headers, buf); + } + } + + @Override + protected boolean isContentAlwaysEmpty(@SuppressWarnings("unused") HttpResponse msg) { + method = queue.poll(); + return HttpMethod.HEAD.equals(method) || super.isContentAlwaysEmpty(msg); + } + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2OrHttp11Handler.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2OrHttp11Handler.java deleted file mode 100644 index 0af06a6f70..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2OrHttp11Handler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty.http2; - -import java.util.function.Consumer; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; - -class Http2OrHttp11Handler extends ApplicationProtocolNegotiationHandler { - - private final Consumer http2; - private final Consumer http1; - - public Http2OrHttp11Handler(Consumer http1, Consumer http2) { - super(ApplicationProtocolNames.HTTP_1_1); - this.http2 = http2; - this.http1 = http1; - } - - @Override - public void configurePipeline(final ChannelHandlerContext ctx, final String protocol) { - if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - http1.accept(ctx.pipeline()); - } else if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - http2.accept(ctx.pipeline()); - } else { - throw new IllegalStateException("Unknown protocol: " + protocol); - } - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2PrefaceOrHttpHandler.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2PrefaceOrHttpHandler.java deleted file mode 100644 index 51900925d3..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/Http2PrefaceOrHttpHandler.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty.http2; - -import java.util.List; -import java.util.function.Consumer; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.ByteToMessageDecoder; - -class Http2PrefaceOrHttpHandler extends ByteToMessageDecoder { - - private static final int PRI = 0x50524920; - - private Consumer http1; - - private Consumer http2; - - public Http2PrefaceOrHttpHandler( - Consumer http1, Consumer http2) { - this.http1 = http1; - this.http2 = http2; - } - - @Override - protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) { - if (in.readableBytes() < 4) { - return; - } - - if (in.getInt(in.readerIndex()) == PRI) { - http2.accept(ctx.pipeline()); - } else { - http1.accept(ctx.pipeline()); - } - - ctx.pipeline().remove(this); - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/NettyHttp2Configurer.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/NettyHttp2Configurer.java deleted file mode 100644 index 3e0de59faf..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/http2/NettyHttp2Configurer.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty.http2; - -import static io.netty.handler.codec.http.HttpScheme.HTTP; - -import io.jooby.internal.netty.Http2Extension; -import io.netty.channel.ChannelInboundHandler; -import io.netty.handler.codec.http.HttpScheme; -import io.netty.handler.codec.http2.DefaultHttp2Connection; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2FrameLogger; -import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; -import io.netty.handler.logging.LogLevel; - -public class NettyHttp2Configurer { - - public ChannelInboundHandler configure(Http2Extension extension) { - if (extension.isSecure()) { - return new Http2OrHttp11Handler( - extension::http11, - pipeline -> - extension.http2( - pipeline, - settings -> newHttp2Handler(settings.getMaxRequestSize(), HttpScheme.HTTPS))); - } else { - return new Http2PrefaceOrHttpHandler( - pipeline -> - extension.http11Upgrade( - pipeline, - settings -> - new Http2ServerUpgradeCodec( - newHttp2Handler(settings.getMaxRequestSize(), HTTP))), - pipeline -> - extension.http2c( - pipeline, settings -> newHttp2Handler(settings.getMaxRequestSize(), HTTP))); - } - } - - private Http2ConnectionHandler newHttp2Handler(int maxRequestSize, HttpScheme scheme) { - DefaultHttp2Connection connection = new DefaultHttp2Connection(true); - InboundHttp2ToHttpAdapter listener = - new InboundHttp2ToHttpAdapterBuilder(connection) - .propagateSettings(false) - .validateHttpHeaders(true) - .maxContentLength(maxRequestSize) - .build(); - - return new HttpToHttp2ConnectionHandlerBuilder() - .frameListener(listener) - .frameLogger(new Http2FrameLogger(LogLevel.DEBUG)) - .connection(connection) - .httpScheme(scheme) - .build(); - } -} diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index de84f0a75f..d780ac3d85 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -272,7 +272,7 @@ public void rawPath(ServerTestRunner runner) { runner .define( app -> { - app.get("/{code}", ctx -> ctx.getRequestPath()); + app.get("/{code}", Context::getRequestPath); }) .ready( client -> { diff --git a/tests/src/test/java/io/jooby/test/Http2Test.java b/tests/src/test/java/io/jooby/test/Http2Test.java index 09594ab77b..f0405aeda1 100644 --- a/tests/src/test/java/io/jooby/test/Http2Test.java +++ b/tests/src/test/java/io/jooby/test/Http2Test.java @@ -5,9 +5,13 @@ */ package io.jooby.test; +import static io.jooby.test.TestUtil._19kb; +import static okhttp3.RequestBody.create; import static org.junit.jupiter.api.Assertions.assertEquals; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Phaser; @@ -26,19 +30,62 @@ import org.junit.jupiter.api.BeforeAll; import com.google.common.collect.ImmutableMap; -import io.jooby.ServerOptions; -import io.jooby.SneakyThrows; -import io.jooby.StatusCode; +import io.jooby.*; +import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; +import okhttp3.*; import okhttp3.MediaType; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; public class Http2Test { + @ServerTest + public void h2body(ServerTestRunner runner) { + runner + .options(new ServerOptions().setHttp2(true).setSecurePort(8443)) + .define( + app -> { + app.install(new JacksonModule()); + app.post( + "/h2/multipart", + ctx -> { + try (var f = ctx.file("f")) { + return ctx.getScheme() + + ":" + + ctx.getProtocol() + + ":" + + new String(f.bytes(), StandardCharsets.UTF_8); + } + }); + + app.post( + "/h2/body", + ctx -> { + return ctx.getScheme() + ":" + ctx.getProtocol() + ":" + ctx.body(Map.class); + }); + }) + .ready( + (http, https) -> { + https.post( + "/h2/multipart", + new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart( + "f", "19kb.txt", create(_19kb, MediaType.parse("text/plain"))) + .build(), + rsp -> { + assertEquals("https:HTTP/2.0:" + _19kb, rsp.body().string()); + }); + + https.post( + "/h2/body", + create("{\"foo\": \"bar\"}", MediaType.parse("application/json")), + rsp -> { + assertEquals("https:HTTP/2.0:" + "{foo=bar}", rsp.body().string()); + }); + }); + } + @ServerTest public void http2(ServerTestRunner runner) { runner From 0616ce2a934c7c0b17ec71069b38a900b7b859ab Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 17 Feb 2026 10:40:56 -0300 Subject: [PATCH 11/31] build: fix compilation error --- modules/jooby-openapi/src/main/java/module-info.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/jooby-openapi/src/main/java/module-info.java b/modules/jooby-openapi/src/main/java/module-info.java index 9f190460b7..8322c5f6d5 100644 --- a/modules/jooby-openapi/src/main/java/module-info.java +++ b/modules/jooby-openapi/src/main/java/module-info.java @@ -20,7 +20,6 @@ requires io.pebbletemplates; requires jdk.jshell; requires com.google.common; - requires org.checkerframework.checker.qual; requires org.asciidoctor.asciidoctorj.api; requires jakarta.data; requires io.swagger.annotations; From 63beb494a3a204b55829407530d29bdefe832550 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Thu, 19 Feb 2026 23:06:49 +0200 Subject: [PATCH 12/31] jackson3 module --- modules/jooby-jackson3/pom.xml | 54 +++++ .../io/jooby/jackson3/Jackson3Module.java | 192 ++++++++++++++++++ .../java/io/jooby/jackson3/package-info.java | 2 + .../src/main/java/module-info.java | 14 ++ .../jackson3/Jackson3JsonModuleTest.java | 96 +++++++++ modules/pom.xml | 1 + pom.xml | 8 + 7 files changed, 367 insertions(+) create mode 100644 modules/jooby-jackson3/pom.xml create mode 100644 modules/jooby-jackson3/src/main/java/io/jooby/jackson3/Jackson3Module.java create mode 100644 modules/jooby-jackson3/src/main/java/io/jooby/jackson3/package-info.java create mode 100644 modules/jooby-jackson3/src/main/java/module-info.java create mode 100644 modules/jooby-jackson3/src/test/java/io/jooby/jackson3/Jackson3JsonModuleTest.java diff --git a/modules/jooby-jackson3/pom.xml b/modules/jooby-jackson3/pom.xml new file mode 100644 index 0000000000..4fc7b07efd --- /dev/null +++ b/modules/jooby-jackson3/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + + io.jooby + modules + 4.0.16-SNAPSHOT + + jooby-jackson3 + jooby-jackson3 + + + + io.jooby + jooby + ${jooby.version} + + + + + tools.jackson.core + jackson-databind + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + org.mockito + mockito-core + test + + + + tools.jackson.dataformat + jackson-dataformat-xml + test + + + + diff --git a/modules/jooby-jackson3/src/main/java/io/jooby/jackson3/Jackson3Module.java b/modules/jooby-jackson3/src/main/java/io/jooby/jackson3/Jackson3Module.java new file mode 100644 index 0000000000..57e2b8d083 --- /dev/null +++ b/modules/jooby-jackson3/src/main/java/io/jooby/jackson3/Jackson3Module.java @@ -0,0 +1,192 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.jackson3; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.*; +import io.jooby.output.Output; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.type.TypeFactory; + +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +/** + * JSON module using Jackson3: https://jooby.io/modules/jackson3. + * + *

Usage: + * + *

{@code
+ * {
+ *
+ *   install(new Jackson3Module());
+ *
+ *   get("/", ctx -> {
+ *     MyObject myObject = ...;
+ *     // send json
+ *     return myObject;
+ *   });
+ *
+ *   post("/", ctx -> {
+ *     // read json
+ *     MyObject myObject = ctx.body(MyObject.class);
+ *     // send json
+ *     return myObject;
+ *   });
+ * }
+ * }
+ *

+ * For body decoding the client must specify the Content-Type header set to + * application/json. + * + *

You can retrieve the {@link ObjectMapper} via require call: + * + *

{@code
+ * {
+ *
+ *   ObjectMapper mapper = require(ObjectMapper.class);
+ *
+ * }
+ * }
+ * + * @author edgar, kliushnichenko + * @since 4.1.0 + */ +public class Jackson3Module implements Extension, MessageDecoder, MessageEncoder { + private final MediaType mediaType; + + private final ObjectMapper mapper; + + private final TypeFactory typeFactory; + + private final Set> modules = new HashSet<>(); + + private static final Map defaultTypes = new HashMap<>(); + + static { + defaultTypes.put("XmlMapper", MediaType.xml); + } + + /** + * Creates a Jackson module. + * + * @param mapper Object mapper to use. + * @param contentType Content type. + */ + public Jackson3Module(@NonNull ObjectMapper mapper, @NonNull MediaType contentType) { + this.mapper = mapper; + this.typeFactory = mapper.getTypeFactory(); + this.mediaType = contentType; + } + + /** + * Creates a Jackson module. + * + * @param mapper Object mapper to use. + */ + public Jackson3Module(@NonNull ObjectMapper mapper) { + this(mapper, defaultTypes.getOrDefault(mapper.getClass().getSimpleName(), MediaType.json)); + } + + /** + * Creates a Jackson module using the default object mapper from {@link #create(JacksonModule...)}. + */ + public Jackson3Module() { + this(create()); + } + + /** + * Add a Jackson module to the object mapper. This method require a dependency injection framework + * which is responsible for provisioning a module instance. + * + * @param module Module type. + * @return This module. + */ + public Jackson3Module module(Class module) { + modules.add(module); + return this; + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public void install(@NonNull Jooby application) { + application.decoder(mediaType, this); + application.encoder(mediaType, this); + + ServiceRegistry services = application.getServices(); + Class mapperType = mapper.getClass(); + services.put(mapperType, mapper); + services.put(ObjectMapper.class, mapper); + + // Parsing exception as 400 + application.errorCode(StreamReadException.class, StatusCode.BAD_REQUEST); + + application.onStarting(() -> onStarting(application, services, mapperType)); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void onStarting(Jooby application, ServiceRegistry services, Class mapperType) { + if (!modules.isEmpty()) { + var builder = mapper.rebuild(); + for (Class type : modules) { + JacksonModule module = application.require(type); + builder.addModule(module); + } + var newMapper = builder.build(); + services.put(mapperType, newMapper); + services.put(ObjectMapper.class, newMapper); + } + } + + @Override + public Output encode(@NonNull Context ctx, @NonNull Object value) { + var factory = ctx.getOutputFactory(); + ctx.setDefaultResponseType(mediaType); + // let jackson uses his own cache, so wrap the bytes + return factory.wrap(mapper.writeValueAsBytes(value)); + } + + @Override + public Object decode(Context ctx, Type type) throws Exception { + Body body = ctx.body(); + if (body.isInMemory()) { + if (type == JsonNode.class) { + return mapper.readTree(body.bytes()); + } + return mapper.readValue(body.bytes(), typeFactory.constructType(type)); + } else { + try (InputStream stream = body.stream()) { + if (type == JsonNode.class) { + return mapper.readTree(stream); + } + return mapper.readValue(stream, typeFactory.constructType(type)); + } + } + } + + /** + * Default object mapper. + * + * @param modules Extra/additional modules to install. + * @return Object mapper instance. + */ + public static ObjectMapper create(JacksonModule... modules) { + JsonMapper.Builder builder = JsonMapper.builder(); + + Stream.of(modules).forEach(builder::addModule); + + return builder.build(); + } +} diff --git a/modules/jooby-jackson3/src/main/java/io/jooby/jackson3/package-info.java b/modules/jooby-jackson3/src/main/java/io/jooby/jackson3/package-info.java new file mode 100644 index 0000000000..05d1271144 --- /dev/null +++ b/modules/jooby-jackson3/src/main/java/io/jooby/jackson3/package-info.java @@ -0,0 +1,2 @@ +@edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault +package io.jooby.jackson3; diff --git a/modules/jooby-jackson3/src/main/java/module-info.java b/modules/jooby-jackson3/src/main/java/module-info.java new file mode 100644 index 0000000000..5500abfdda --- /dev/null +++ b/modules/jooby-jackson3/src/main/java/module-info.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +/** Jackson module. */ +module io.jooby.jackson3 { + exports io.jooby.jackson3; + + requires io.jooby; + requires static com.github.spotbugs.annotations; + requires typesafe.config; + requires tools.jackson.databind; +} diff --git a/modules/jooby-jackson3/src/test/java/io/jooby/jackson3/Jackson3JsonModuleTest.java b/modules/jooby-jackson3/src/test/java/io/jooby/jackson3/Jackson3JsonModuleTest.java new file mode 100644 index 0000000000..d54ad90ae0 --- /dev/null +++ b/modules/jooby-jackson3/src/test/java/io/jooby/jackson3/Jackson3JsonModuleTest.java @@ -0,0 +1,96 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.jackson3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.dataformat.xml.XmlMapper; +import io.jooby.Body; +import io.jooby.Context; +import io.jooby.MediaType; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; + +public class Jackson3JsonModuleTest { + + @Test + public void renderJson() { + Context ctx = mock(Context.class); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); + + Jackson3Module jackson = new Jackson3Module(new ObjectMapper()); + + var buffer = jackson.encode(ctx, mapOf("k", "v")); + assertEquals("{\"k\":\"v\"}", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); + + verify(ctx).setDefaultResponseType(MediaType.json); + } + + @Test + public void parseJson() throws Exception { + byte[] bytes = "{\"k\":\"v\"}".getBytes(StandardCharsets.UTF_8); + Body body = mock(Body.class); + when(body.isInMemory()).thenReturn(true); + when(body.bytes()).thenReturn(bytes); + + Context ctx = mock(Context.class); + when(ctx.body()).thenReturn(body); + + Jackson3Module jackson = new Jackson3Module(new ObjectMapper()); + + Map result = (Map) jackson.decode(ctx, Map.class); + assertEquals(mapOf("k", "v"), result); + } + + @Test + public void renderXml() { + Context ctx = mock(Context.class); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); + + Jackson3Module jackson = new Jackson3Module(new XmlMapper()); + + var buffer = jackson.encode(ctx, mapOf("k", "v")); + assertEquals( + "v", + StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); + + verify(ctx).setDefaultResponseType(MediaType.xml); + } + + @Test + public void parseXml() throws Exception { + byte[] bytes = "v".getBytes(StandardCharsets.UTF_8); + Body body = mock(Body.class); + when(body.isInMemory()).thenReturn(true); + when(body.bytes()).thenReturn(bytes); + + Context ctx = mock(Context.class); + when(ctx.body()).thenReturn(body); + + Jackson3Module jackson = new Jackson3Module(new XmlMapper()); + + Map result = (Map) jackson.decode(ctx, Map.class); + assertEquals(mapOf("k", "v"), result); + } + + private Map mapOf(String... values) { + Map hash = new HashMap<>(); + for (int i = 0; i < values.length; i += 2) { + hash.put(values[i], values[i + 1]); + } + return hash; + } +} diff --git a/modules/pom.xml b/modules/pom.xml index 8af7217521..5277cd3326 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -48,6 +48,7 @@ jooby-caffeine jooby-jackson + jooby-jackson3 jooby-gson jooby-avaje-jsonb jooby-yasson diff --git a/pom.xml b/pom.xml index 3c64919c20..166efb7f26 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ 1.3.7 4.1.1 2.21.0 + 3.0.4 2.13.2 3.0.1 3.0.4 @@ -254,6 +255,13 @@ pom import + + tools.jackson + jackson-bom + ${jackson3.version} + pom + import + io.jooby From f9e9a902ef6e2ac0c70b0c9a28610960c99b50d1 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Fri, 20 Feb 2026 20:39:58 +0200 Subject: [PATCH 13/31] jackson3 docs --- .../modules/{jackson.adoc => jackson2.adoc} | 30 +-- docs/asciidoc/modules/jackson3.adoc | 183 ++++++++++++++++++ docs/asciidoc/modules/modules.adoc | 3 +- docs/pom.xml | 2 +- .../java/io/jooby/jackson/JacksonModule.java | 4 +- pom.xml | 6 + 6 files changed, 209 insertions(+), 19 deletions(-) rename docs/asciidoc/modules/{jackson.adoc => jackson2.adoc} (80%) create mode 100644 docs/asciidoc/modules/jackson3.adoc diff --git a/docs/asciidoc/modules/jackson.adoc b/docs/asciidoc/modules/jackson2.adoc similarity index 80% rename from docs/asciidoc/modules/jackson.adoc rename to docs/asciidoc/modules/jackson2.adoc index 13eaa4f927..331ed1beed 100644 --- a/docs/asciidoc/modules/jackson.adoc +++ b/docs/asciidoc/modules/jackson2.adoc @@ -1,4 +1,4 @@ -== Jackson +== Jackson 2 JSON support using https://github.com/FasterXML/jackson[Jackson] library. @@ -14,7 +14,7 @@ JSON support using https://github.com/FasterXML/jackson[Jackson] library. .Java [source, java, role="primary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { install(new JacksonModule()); <1> @@ -34,7 +34,7 @@ import io.jooby.json.JacksonModule; .Kotlin [source, kt, role="secondary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { install(JacksonModule()) <1> @@ -62,7 +62,7 @@ Access to default object mapper is available via require call: .Default object mapper [source, java, role="primary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { install(new JacksonModule()); @@ -76,7 +76,7 @@ import io.jooby.json.JacksonModule; .Kotlin [source, kt, role="secondary"] ---- -import io.jooby.json.JacksonModule +import io.jooby.jackson.JacksonModule { install(JacksonModule()) @@ -90,7 +90,7 @@ You can provide your own `ObjectMapper`: .Custom ObjectMapper [source, java, role="primary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { ObjectMapper mapper = new ObjectMapper(); @@ -102,7 +102,7 @@ import io.jooby.json.JacksonModule; .Kotlin [source, kt, role="secondary"] ---- -import io.jooby.json.JacksonModule +import io.jooby.jackson.JacksonModule { val mapper = ObjectMapper() @@ -116,7 +116,7 @@ This allows to configure JacksonModule for doing `xml` processing: .XmlMapper [source, java, role="primary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { install(new JacksonModule(new XmlMapper())); @@ -126,19 +126,19 @@ import io.jooby.json.JacksonModule; .Kotlin [source, kt, role="secondary"] ---- -import io.jooby.json.JacksonModule +import io.jooby.jackson.JacksonModule { install(JacksonModule(XmlMapper())) } ---- -If you want `jackson` and `xml` processing then install twice: +If you want `json` and `xml` processing then install twice: .XmlMapper [source, java, role="primary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { install(new JacksonModule(new ObjectMapper())); @@ -149,7 +149,7 @@ import io.jooby.json.JacksonModule; .Kotlin [source, kt, role="secondary"] ---- -import io.jooby.json.JacksonModule +import io.jooby.jackson.JacksonModule { install(JacksonModule(ObjectMapper())) @@ -164,7 +164,7 @@ Jackson module can be provided by a link:{uiVersion}/#extensions-and-services-de .Provisioning Modules [source, java, role="primary"] ---- -import io.jooby.json.JacksonModule; +import io.jooby.jackson.JacksonModule; { install(new JacksonModule().module(MyModule.class); @@ -174,11 +174,11 @@ import io.jooby.json.JacksonModule; .Kotlin [source, kt, role="secondary"] ---- -import io.jooby.json.JacksonModule +import io.jooby.jackson.JacksonModule { install(JacksonModule().module(MyModule::class.java) } ---- -At startup time Jooby ask to dependency injection framework to provide a `MyModule` instance. +At startup time Jooby ask dependency injection framework to provide a `MyModule` instance. diff --git a/docs/asciidoc/modules/jackson3.adoc b/docs/asciidoc/modules/jackson3.adoc new file mode 100644 index 0000000000..6d18172048 --- /dev/null +++ b/docs/asciidoc/modules/jackson3.adoc @@ -0,0 +1,183 @@ +== Jackson 3 + +JSON support using https://github.com/FasterXML/jackson[Jackson 3] library. + +=== Usage + +1) Add the dependency: + +[dependency, artifactId="jooby-jackson3"] +. + +2) Install and encode/decode JSON + +.Java +[source, java, role="primary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + install(new Jackson3Module()); <1> + + get("/", ctx -> { + MyObject myObject = ...; + return myObject; <2> + }); + + post("/", ctx -> { + MyObject myObject = ctx.body(MyObject.class); <3> + ... + }); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + install(Jackson3Module()) <1> + + get("/") { + val myObject = ...; + myObject <2> + } + + post("/") { + val myObject = ctx.body() <3> + ... + } +} +---- + +<1> Install Jackson +<2> Use Jackson to encode arbitrary object as JSON +<3> Use Jackson to decode JSON to Java object. Client must specify the `Content-Type: application/json` header + +=== Working with ObjectMapper + +Access to default object mapper is available via require call: + +.Default object mapper +[source, java, role="primary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + install(new Jackson3Module()); + + ObjectMapper mapper = require(ObjectMapper.class); + ... +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.jackson3.Jackson3Module + +{ + install(Jackson3Module()) + + val mapper = require() +} +---- + +You can provide your own `ObjectMapper`: + +.Custom ObjectMapper +[source, java, role="primary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + ObjectMapper mapper = new ObjectMapper(); + + install(new Jackson3Module(mapper)); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.jackson3.Jackson3Module + +{ + val mapper = ObjectMapper() + + install(Jackson3Module(mapper)) +} +---- + +This allows to configure `Jackson3Module` for doing `xml` processing: + +.XmlMapper +[source, java, role="primary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + install(new Jackson3Module(new XmlMapper())); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.jackson3.Jackson3Module + +{ + install(Jackson3Module(XmlMapper())) +} +---- + +If you want `json` and `xml` processing then install twice: + +.XmlMapper+JsonMapper +[source, java, role="primary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + install(new Jackson3Module(new JsonMapper())); + install(new Jackson3Module(new XmlMapper())); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.jackson3.Jackson3Module + +{ + install(Jackson3Module(JsonMapper())) + install(Jackson3Module(XmlMapper())) +} +---- + +=== Provisioning Jackson Modules + +Jackson module can be provided by a link:{uiVersion}/#extensions-and-services-dependency-injection[dependency injection] framework. + +.Provisioning Modules +[source, java, role="primary"] +---- +import io.jooby.jackson3.Jackson3Module; + +{ + install(new Jackson3Module().module(MyModule.class); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.jackson3.Jackson3Module + +{ + install(Jackson3Module().module(MyModule::class.java) +} +---- + +At startup time Jooby asks dependency injection framework to provide a `MyModule` instance. diff --git a/docs/asciidoc/modules/modules.adoc b/docs/asciidoc/modules/modules.adoc index 4d58f29427..b5cbd51c1f 100644 --- a/docs/asciidoc/modules/modules.adoc +++ b/docs/asciidoc/modules/modules.adoc @@ -44,7 +44,8 @@ Available modules are listed next. === JSON * link:{uiVersion}/modules/gson[Gson]: Gson module for Jooby. - * link:{uiVersion}/modules/jackson[Jackson]: Jackson module for Jooby. + * link:{uiVersion}/modules/jackson2[Jackson2]: Jackson2 module for Jooby. + * link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby. * link:{uiVersion}/modules/yasson[JSON-B]: JSON-B module for Jooby. * link:{uiVersion}/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby. diff --git a/docs/pom.xml b/docs/pom.xml index 6b665b074a..8ad18c7d27 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -10,7 +10,7 @@ io.jooby.adoc.DocApp - 4.0.7 + 4.0.15 17 17 17 diff --git a/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java b/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java index 2cd82c291c..70633961cb 100644 --- a/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java +++ b/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java @@ -35,7 +35,7 @@ import io.jooby.output.Output; /** - * JSON module using Jackson: https://jooby.io/modules/jackson. + * JSON module using Jackson: https://jooby.io/modules/jackson2. * *

Usage: * @@ -72,7 +72,7 @@ * } * } * - * Complete documentation is available at: https://jooby.io/modules/jackson. + * Complete documentation is available at: https://jooby.io/modules/jackson2. * * @author edgar * @since 2.0.0 diff --git a/pom.xml b/pom.xml index 166efb7f26..5cc67431f5 100644 --- a/pom.xml +++ b/pom.xml @@ -318,6 +318,12 @@ ${jooby.version} + + io.jooby + jooby-jackson3 + ${jooby.version} + + io.jooby jooby-gson From c84a45c542d82911567af9be524df69ee34ba353 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 21 Feb 2026 12:02:36 -0300 Subject: [PATCH 14/31] build: make some json tests to run on Jackson3 module --- tests/pom.xml | 5 +++++ tests/src/test/java/io/jooby/test/FeaturedTest.java | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/pom.xml b/tests/pom.xml index 14242d8834..af03d12d2c 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -36,6 +36,11 @@ jooby-jackson ${jooby.version} + + io.jooby + jooby-jackson3 + ${jooby.version} + io.jooby jooby-gson diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index d780ac3d85..84352b972f 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -67,6 +67,7 @@ import io.jooby.handler.TraceHandler; import io.jooby.handler.WebVariables; import io.jooby.jackson.JacksonModule; +import io.jooby.jackson3.Jackson3Module; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; import io.jooby.netty.NettyServer; @@ -912,7 +913,7 @@ public void decoder(ServerTestRunner runner) { runner .define( app -> { - app.install(new JacksonModule()); + app.install(new Jackson3Module()); app.post("/map", ctx -> ctx.body(Map.class)); @@ -971,7 +972,7 @@ public void jsonVsRawOutput(ServerTestRunner runner) { runner .define( app -> { - app.install(new JacksonModule()); + app.install(new Jackson3Module()); app.path( "/api/pets", @@ -1211,7 +1212,7 @@ public void errorHandler(ServerTestRunner runner) { runner .define( app -> { - app.install(new JacksonModule()); + app.install(new Jackson3Module()); app.get( "/", From 11e0c6f1fc16d2f0005545d7de79c59eb866ab4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:03:02 +0000 Subject: [PATCH 15/31] build(deps): bump swagger-ui-dist in /modules/jooby-swagger-ui Bumps [swagger-ui-dist](https://github.com/swagger-api/swagger-ui) from 5.31.0 to 5.31.2. - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.31.0...v5.31.2) --- updated-dependencies: - dependency-name: swagger-ui-dist dependency-version: 5.31.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- modules/jooby-swagger-ui/package-lock.json | 8 ++++---- modules/jooby-swagger-ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/jooby-swagger-ui/package-lock.json b/modules/jooby-swagger-ui/package-lock.json index 4416d58c38..00948a416e 100644 --- a/modules/jooby-swagger-ui/package-lock.json +++ b/modules/jooby-swagger-ui/package-lock.json @@ -9,7 +9,7 @@ "version": "4.0.0", "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.31.0" + "swagger-ui-dist": "^5.31.2" } }, "node_modules/@scarf/scarf": { @@ -20,9 +20,9 @@ "license": "Apache-2.0" }, "node_modules/swagger-ui-dist": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", - "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "version": "5.31.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.2.tgz", + "integrity": "sha512-uIoesCjDcxnAKj/C/HG5pjHZMQs2K/qmqpUlwLxxaVryGKlgm8Ri+VOza5xywAqf//pgg/hW16RYa6dDuTCOSg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/modules/jooby-swagger-ui/package.json b/modules/jooby-swagger-ui/package.json index 0ddbf68e46..ce24a7a792 100644 --- a/modules/jooby-swagger-ui/package.json +++ b/modules/jooby-swagger-ui/package.json @@ -4,7 +4,7 @@ "private": true, "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.31.0" + "swagger-ui-dist": "^5.31.2" }, "scarfSettings": { "enabled": false From b8c098e4a1f95b937d43639871d46090c01743ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:08:26 +0000 Subject: [PATCH 16/31] build(deps): bump the dependencies group with 16 updates Bumps the dependencies group with 16 updates: | Package | From | To | | --- | --- | --- | | [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) | `1.5.31` | `1.5.32` | | io.swagger.core.v3:swagger-annotations | `2.2.42` | `2.2.43` | | io.swagger.core.v3:swagger-models | `2.2.42` | `2.2.43` | | [io.swagger.parser.v3:swagger-parser](https://github.com/swagger-api/swagger-parser) | `2.1.37` | `2.1.38` | | [io.ebean:ebean](https://github.com/ebean-orm/ebean) | `17.2.1` | `17.3.0` | | [io.ebean:ebean-querybean](https://github.com/ebean-orm/ebean) | `17.2.1` | `17.3.0` | | [io.ebean:querybean-generator](https://github.com/ebean-orm/ebean) | `17.2.1` | `17.3.0` | | [io.ebean:ebean-test](https://github.com/ebean-orm/ebean) | `17.2.1` | `17.3.0` | | org.apache.kafka:kafka-clients | `4.1.1` | `4.2.0` | | [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.5.4` | `3.5.5` | | [io.vertx:vertx-core](https://github.com/eclipse/vert.x) | `5.0.7` | `5.0.8` | | [io.vertx:vertx-sql-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.7` | `5.0.8` | | [io.vertx:vertx-mysql-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.7` | `5.0.8` | | [io.vertx:vertx-pg-client](https://github.com/eclipse-vertx/vertx-sql-client) | `5.0.7` | `5.0.8` | | software.amazon.awssdk:bom | `2.41.29` | `2.41.34` | | [io.smallrye.reactive:mutiny](https://github.com/smallrye/smallrye-mutiny) | `3.1.0` | `3.1.1` | Updates `ch.qos.logback:logback-classic` from 1.5.31 to 1.5.32 - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.31...v_1.5.32) Updates `io.swagger.core.v3:swagger-annotations` from 2.2.42 to 2.2.43 Updates `io.swagger.core.v3:swagger-models` from 2.2.42 to 2.2.43 Updates `io.swagger.core.v3:swagger-models` from 2.2.42 to 2.2.43 Updates `io.swagger.parser.v3:swagger-parser` from 2.1.37 to 2.1.38 - [Release notes](https://github.com/swagger-api/swagger-parser/releases) - [Commits](https://github.com/swagger-api/swagger-parser/compare/v2.1.37...v2.1.38) Updates `io.ebean:ebean` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-querybean` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:querybean-generator` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-test` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-querybean` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:querybean-generator` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-test` from 17.2.1 to 17.3.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `org.apache.kafka:kafka-clients` from 4.1.1 to 4.2.0 Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5) Updates `io.vertx:vertx-core` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse/vert.x/compare/5.0.7...5.0.8) Updates `io.vertx:vertx-sql-client` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.7...5.0.8) Updates `io.vertx:vertx-mysql-client` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.7...5.0.8) Updates `io.vertx:vertx-pg-client` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.7...5.0.8) Updates `io.vertx:vertx-sql-client` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.7...5.0.8) Updates `io.vertx:vertx-mysql-client` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.7...5.0.8) Updates `io.vertx:vertx-pg-client` from 5.0.7 to 5.0.8 - [Commits](https://github.com/eclipse-vertx/vertx-sql-client/compare/5.0.7...5.0.8) Updates `software.amazon.awssdk:bom` from 2.41.29 to 2.41.34 Updates `io.smallrye.reactive:mutiny` from 3.1.0 to 3.1.1 - [Release notes](https://github.com/smallrye/smallrye-mutiny/releases) - [Commits](https://github.com/smallrye/smallrye-mutiny/compare/3.1.0...3.1.1) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.5.32 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-annotations dependency-version: 2.2.43 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.43 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.43 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.parser.v3:swagger-parser dependency-version: 2.1.38 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-querybean dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:querybean-generator dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-test dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-querybean dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:querybean-generator dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-test dependency-version: 17.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.apache.kafka:kafka-clients dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-core dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-sql-client dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-mysql-client dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-pg-client dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-sql-client dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-mysql-client dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.vertx:vertx-pg-client dependency-version: 5.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.41.34 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.smallrye.reactive:mutiny dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- pom.xml | 14 +++++++------- tests/pom.xml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 4a7cddb135..89f3b400c3 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.41.29 + 2.41.34 diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 61218f746c..7499ea30dd 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -26,7 +26,7 @@ io.smallrye.reactive mutiny - 3.1.0 + 3.1.1 diff --git a/pom.xml b/pom.xml index 5cc67431f5..8214a7539b 100644 --- a/pom.xml +++ b/pom.xml @@ -74,13 +74,13 @@ 7.0.2 1.2 7.0.4.Final - 17.2.1 + 17.3.0 3.51.0 11.20.1 25.0 7.4.0.RELEASE 2.13.1 - 4.1.1 + 4.2.0 3.2.3 1.4.5 @@ -89,7 +89,7 @@ 7.0.0 - 1.5.31 + 1.5.32 2.25.3 2.0.17 @@ -109,11 +109,11 @@ 2.3.23.Final 12.1.6 4.2.10.Final - 5.0.7 + 5.0.8 - 2.2.42 - 2.1.37 + 2.2.43 + 2.1.38 2.0.0-rc.20 @@ -188,7 +188,7 @@ 3.6.1 3.8.2 3.4.0 - 3.5.4 + 3.5.5 2.3.1 4.0.2 3.2.0 diff --git a/tests/pom.xml b/tests/pom.xml index af03d12d2c..fc3db481e5 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -211,7 +211,7 @@ io.vertx vertx-pg-client - 5.0.7 + 5.0.8 From 4cd18a01d5b6f862ddf4ef1cad43c3c1f361e9c3 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 24 Feb 2026 10:44:20 -0300 Subject: [PATCH 17/31] documentation: find a way to organize/group/classify documentation better MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` Jooby Documentation ├── 1. Getting Started │ ├── Quick Start │ ├── Alternatives (Kotlin/Java) │ └── Jooby CLI ├── 2. Core │ ├── Router │ │ ├── Defining Routes │ │ ├── Pipeline │ │ ├── Advanced Routing │ │ └── Filter vs Handler │ ├── Context (Request) │ │ ├── Parameters │ │ ├── Value API │ │ ├── Request Body │ │ └── Projection API │ ├── Responses │ │ ├── Renderers │ │ └── Non-blocking Responses │ ├── Built-in Handlers │ │ ├── Access Log │ │ ├── CORS │ │ ├── CSRF │ │ ├── Graceful Shutdown │ │ ├── HEAD Support │ │ ├── Rate Limit │ │ ├── SSL Redirect │ │ └── TRACE Support │ ├── Error Handler │ │ └── Problem Details (RFC 7807) │ └── Execution Model │ ├── Event Loop │ └── Worker Threads (Loom) ├── 3. Extensions (Modules) │ ├── What are Extensions? │ ├── Dependency Injection │ │ ├── Guice │ │ ├── Avaje Inject │ │ └── Dagger │ ├── Data Access (Hibernate, JDBI, etc.) │ ├── JSON & Serialization (Jackson, GSON) │ └── Template Engines (Pebble, Freemarker) ├── 4. Tooling and Operations │ ├── Configuration (Environment & Logging) │ ├── Server (Netty, Jetty, Undertow, Vert.x) │ ├── Development (Hot Reload / jooby:run) │ ├── Testing (Unit & Integration) │ └── Packaging (Fat Jar & Stork) └── 5. MVC API ├── Controller Definition ├── Dependency Injection in MVC └── Result Mapping ``` fix #3855 --- docs/asciidoc/body.adoc | 117 ++-- docs/asciidoc/configuration.adoc | 379 +++--------- docs/asciidoc/context.adoc | 378 ++++-------- docs/asciidoc/core.adoc | 13 + docs/asciidoc/dev-tools.adoc | 120 ++-- docs/asciidoc/docinfo-footer.html | 38 +- docs/asciidoc/docinfo.html | 118 ++-- docs/asciidoc/ecosystem.adoc | 207 +++++++ docs/asciidoc/error-handler.adoc | 162 +++-- docs/asciidoc/execution-model.adoc | 214 +++---- docs/asciidoc/extension.adoc | 152 ----- docs/asciidoc/getting-started.adoc | 166 +---- docs/asciidoc/handlers.adoc | 4 +- docs/asciidoc/handlers/access-log.adoc | 53 +- docs/asciidoc/handlers/cors.adoc | 109 ++-- docs/asciidoc/handlers/csrf.adoc | 88 ++- docs/asciidoc/handlers/graceful-shutdown.adoc | 38 +- docs/asciidoc/handlers/head.adoc | 41 +- docs/asciidoc/handlers/rate-limit.adoc | 99 +-- docs/asciidoc/handlers/ssl.adoc | 67 +-- docs/asciidoc/handlers/trace.adoc | 40 +- docs/asciidoc/index.adoc | 221 +------ docs/asciidoc/intro.adoc | 114 ++++ docs/asciidoc/migration.adoc | 2 +- docs/asciidoc/modules/jackson2.adoc | 10 +- docs/asciidoc/modules/jackson3.adoc | 8 +- docs/asciidoc/modules/modules.adoc | 127 ++-- docs/asciidoc/modules/whoops.adoc | 2 +- docs/asciidoc/mvc-api.adoc | 568 +++++++----------- docs/asciidoc/packaging/packaging.adoc | 152 ++--- docs/asciidoc/problem-details.adoc | 330 ++++++---- docs/asciidoc/quick-start.adoc | 133 ++++ docs/asciidoc/responses.adoc | 351 ++++------- docs/asciidoc/router-hidden-method.adoc | 38 +- docs/asciidoc/router-options.adoc | 86 ++- docs/asciidoc/routing.adoc | 428 ++++++------- docs/asciidoc/server-sent-event.adoc | 50 +- docs/asciidoc/servers.adoc | 528 +++------------- docs/asciidoc/session.adoc | 130 ++-- docs/asciidoc/static-files.adoc | 160 ++--- docs/asciidoc/templates.adoc | 102 ++-- docs/asciidoc/testing.adoc | 433 ++++--------- docs/asciidoc/tooling.adoc | 11 + docs/asciidoc/value-api.adoc | 319 +++++----- docs/asciidoc/web.adoc | 11 + docs/asciidoc/websocket.adoc | 94 ++- docs/js/styles/toc.css | 104 ++++ docs/js/toc.js | 78 +++ docs/js/tocbot.min.js | 1 - .../main/java/io/jooby/adoc/DocGenerator.java | 5 +- .../java/io/jooby/adoc/DocPostprocessor.java | 45 +- 51 files changed, 3030 insertions(+), 4214 deletions(-) create mode 100644 docs/asciidoc/core.adoc create mode 100644 docs/asciidoc/ecosystem.adoc delete mode 100644 docs/asciidoc/extension.adoc create mode 100644 docs/asciidoc/intro.adoc create mode 100644 docs/asciidoc/quick-start.adoc create mode 100644 docs/asciidoc/tooling.adoc create mode 100644 docs/asciidoc/web.adoc create mode 100644 docs/js/styles/toc.css create mode 100644 docs/js/toc.js delete mode 100755 docs/js/tocbot.min.js diff --git a/docs/asciidoc/body.adoc b/docs/asciidoc/body.adoc index bbb0ceb00c..de1fb3a1b4 100644 --- a/docs/asciidoc/body.adoc +++ b/docs/asciidoc/body.adoc @@ -1,6 +1,6 @@ -=== Request Body +==== Request Body -Raw `request body` is available via javadoc:Context[body] method: +The raw request body is available via the javadoc:Context[body] method: .Java [source,java,role="primary"] @@ -8,17 +8,17 @@ Raw `request body` is available via javadoc:Context[body] method: { post("/string", ctx -> { String body = ctx.body().value(); // <1> - ... + // ... }); post("/bytes", ctx -> { byte[] body = ctx.body().bytes(); // <2> - ... + // ... }); post("/stream", ctx -> { InputStream body = ctx.body().stream(); // <3> - ... + // ... }); } ---- @@ -29,57 +29,52 @@ Raw `request body` is available via javadoc:Context[body] method: { post("/string") { val body = ctx.body().value() // <1> - ... + // ... } post("/bytes") { val body = ctx.body().bytes() // <2> - ... + // ... } post("/stream") { val body = ctx.body().stream() // <3> - ... + // ... } } ---- -<1> `HTTP Body` as `String` -<2> `HTTP Body` as `byte array` -<3> `HTTP Body` as `InputStream` +<1> Reads the HTTP body as a `String`. +<2> Reads the HTTP body as a `byte array`. +<3> Reads the HTTP body as an `InputStream`. -This gives us the `raw body`. +===== Message Decoder -==== Message Decoder - -Request body parsing is achieved using the javadoc:MessageDecoder[] functional interface. +Request body parsing (converting the raw body into a specific object) is handled by the javadoc:MessageDecoder[] functional interface. [source, java] ---- public interface MessageDecoder { - T decode(Context ctx, Type type) throws Exception; } ---- -javadoc:MessageDecoder[] has a single `decode` method that takes two input arguments: `(context, type)` -and returns a single result of the given type. +The javadoc:MessageDecoder[] has a single `decode` method that takes the request context and the target type, returning the parsed result. -.JSON example: +.JSON Decoder Example: [source, java, role="primary"] ---- { FavoriteJson lib = new FavoriteJson(); // <1> - decoder(MediaType.json, (ctx, type) -> { // <2> - + decoder(MediaType.json, (ctx, type) -> { // <2> byte[] body = ctx.body().bytes(); // <3> - return lib.fromJson(body, type); // <4> }); post("/", ctx -> { MyObject myObject = ctx.body(MyObject.class); // <5> + return myObject; }); } ---- @@ -90,40 +85,37 @@ and returns a single result of the given type. { val lib = FavoriteJson() // <1> - decoder(MediaType.json) { ctx, type -> // <2> - + decoder(MediaType.json) { ctx, type -> // <2> val body = ctx.body().bytes() // <3> - lib.fromJson(body, type) // <4> } post("/") { val myObject = ctx.body() // <5> + myObject } } ---- -<1> Choose your favorite `json` library -<2> Check if the `Content-Type` header matches `application/json` -<3> Read the body as `byte[]` -<4> Parse the `body` and use the requested type -<5> Route handler now call the `body(Type)` function to trigger the decoder function +<1> Initialize your favorite JSON library. +<2> Register the decoder to trigger when the `Content-Type` header matches `application/json`. +<3> Read the raw body as a `byte[]`. +<4> Parse the payload into the requested type. +<5> Inside the route, calling `ctx.body(Type)` automatically triggers the registered decoder. -=== Response Body +==== Response Body -Response body is generated from `handler` function: +The response body is generated by the route handler. -.Response body +.Response Body Example [source, java,role="primary"] ---- { get("/", ctx -> { - ctx.setResponseCode(200); // <1> - + ctx.setResponseCode(200); // <1> ctx.setResponseType(MediaType.text); // <2> - ctx.setResponseHeader("Date", new Date()); // <3> - + return "Response"; // <4> }); } @@ -135,56 +127,46 @@ Response body is generated from `handler` function: { get("/") { ctx.responseCode = 200 // <1> - ctx.responseType = MediaType.text // <2> - ctx.setResponseHeader("Date", Date()) // <3> - + "Response" // <4> } } ---- -<1> Set `status code` to `OK(200)`. This is the default `status code` -<2> Set `content-type` to `text/plain`. This is the default `content-type` -<3> Set the `date` header -<4> Send a `Response` string to the client +<1> Set the status code to `200 OK` (this is the default). +<2> Set the `Content-Type` to `text/plain` (this is the default for strings). +<3> Set a custom response header. +<4> Return the response body to the client. -==== Message Encoder +===== Message Encoder -Response encoding is achieved using the javadoc:MessageEncoder[] functional interface. +Response encoding (converting an object into a raw HTTP response) is handled by the javadoc:MessageEncoder[] functional interface. [source, java] ---- public interface MessageEncoder { - Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception; } ---- -MessageEncoder has a single `encode` method that accepts two input arguments: `(context, value)` and -produces a result. +The javadoc:MessageEncoder[] has a single `encode` method that accepts the context and the value returned by the handler, producing an output. (Internally, javadoc:output.Output[] works like a `java.nio.ByteBuffer` for performance reasons). -The javadoc:output.Output[] works like `java.nio.ByteBuffer` and it is used internally -for performance reason. - -.JSON example: +.JSON Encoder Example: [source, java, role="primary"] ---- { FavoriteJson lib = new FavoriteJson(); // <1> - encoder(MediaType.json, (ctx, result) -> { // <2> - + encoder(MediaType.json, (ctx, result) -> { // <2> String json = lib.toJson(result); // <3> - ctx.setDefaultResponseType(MediaType.json); // <4> - return json; // <5> }); get("/item", ctx -> { - MyObject myObject = ...; + MyObject myObject = new MyObject(); return myObject; // <6> }); } @@ -196,25 +178,22 @@ for performance reason. { val lib = FavoriteJson() // <1> - encoder(MediaType.json) { ctx, result -> // <2> - + encoder(MediaType.json) { ctx, result -> // <2> val json = lib.toJson(result) // <3> - ctx.defaultResponseType = MediaType.json // <4> - json // <5> } get("/item") { - val myObject = ...; + val myObject = MyObject() myObject // <6> } } ---- -<1> Choose your favorite `json` library -<2> Check if the `Accept` header matches `application/json` -<3> Convert `result` to `JSON` -<4> Set default `Content-Type` to `application/json` -<5> Produces JSON response -<6> Route handler returns a user defined type +<1> Initialize your favorite JSON library. +<2> Register the encoder to trigger when the client's `Accept` header matches `application/json`. +<3> Convert the route's result into JSON. +<4> Set the `Content-Type` header to `application/json`. +<5> Return the encoded JSON payload. +<6> The route handler returns a user-defined POJO, which is automatically intercepted and encoded. diff --git a/docs/asciidoc/configuration.adoc b/docs/asciidoc/configuration.adoc index a8cfda1809..9e637bcc60 100644 --- a/docs/asciidoc/configuration.adoc +++ b/docs/asciidoc/configuration.adoc @@ -1,19 +1,16 @@ -== Configuration -Application configuration is based on https://github.com/lightbend/config[config] library. Configuration -can by default be provided in either Java properties, JSON, and https://github.com/lightbend/config/blob/master/HOCON.md[HOCON] files. +=== Configuration -Jooby allows overriding any property via system properties or environment variables. +Application configuration is built on the https://github.com/lightbend/config[Typesafe Config] library. By default, Jooby supports configuration provided in Java properties, JSON, or https://github.com/lightbend/config/blob/master/HOCON.md[HOCON] format. -=== Environment +Jooby allows you to override any property via system properties, environment variables, or program arguments. -The application environment is available via the javadoc:Environment[Environment] class, which allows specifying one -or many unique environment names. +==== Environment -The active environment names serve the purpose of allowing loading different configuration files -depending on the environment. Also, javadoc:Extension[] modules might configure application -services differently depending on the environment too. For example: turn on/off caches, reload files, etc. +The javadoc:Environment[Environment] class manages your application's configuration and active environment names (e.g., `dev`, `prod`, `test`). -.Initializing the Environment +Environment names allow you to load different configuration files or toggle features (like caching or file reloading) depending on the deployment stage. + +.Accessing the Environment [source, java, role = "primary"] ---- { @@ -29,85 +26,42 @@ services differently depending on the environment too. For example: turn on/off } ---- -The active environment names property is set in one of this way: - -- As program argument: `java -jar myapp.jar application.env=foo,bar`; or just `java -jar myapp.jar foo,bar` - -NOTE: This method works as long you start the application using one of the `runApp` methods - -- As system property: `java -Dapplication.env=foo,bar -jar myapp.jar` - -- As environment variable: `application.env=foo,bar` - - -The javadoc:Jooby[getEnvironment] loads the default environment. - -=== Default Environment - -The default environment is available via javadoc:Environment[loadEnvironment, io.jooby.EnvironmentOptions] method. - -This method search for an `application.conf` file in three location (first-listed are higher priority): - -- `${user.dir}/conf`. This is a file system location, useful is you want to externalize configuration (outside of jar file) -- `${user.dir}`. This is a file system location, useful is you want to externalize configuration (outside of jar file) -- `classpath://` (root of classpath). No external configuration, configuration file lives inside the jar file - -NOTE: We use `$user.dir` to reference `System.getProperty("user.dir")`. This system property is set -by the JVM at application startup time. It represent the current directory from where the JVM was -launch it. - -.File system loading -[source,bash] ----- -└── conf - └── application.conf -└── myapp.jar ----- - -A call to: +You can set active environment names in several ways: -[source] ----- - Environment env = getEnvironment(); ----- +* **Program Argument:** `java -jar myapp.jar prod,cloud` (This works when using Jooby's `runApp` methods). +* **System Property:** `java -Dapplication.env=prod -jar myapp.jar` +* **Environment Variable:** `application.env=prod` -Loads the `application.conf` from `conf` directory. You get the same thing if you -move the `application.conf` to `myapp.jar` directory. +==== Default Loading and Precedence -.Classpath loading -[source,bash] ----- -└── myapp.jar - └── application.conf (file inside jar) ----- +When you call `getEnvironment()`, Jooby searches for an `application.conf` file in the following order of priority: -WARNING: Jooby favors file system property loading over classpath property loading. So, if there -is a property file either in the current directory or conf directory it hides the same file -available in the classpath. +1. `${user.dir}/conf/application.conf` (External file system) +2. `${user.dir}/application.conf` (External file system) +3. `classpath://application.conf` (Internal jar resource) -=== Overrides +[NOTE] +==== +`${user.dir}` refers to the directory from which the JVM was launched. Jooby **favors file system files** over classpath files, allowing you to easily externalize configuration without rebuilding your jar. +==== -Property overrides is done in multiple ways (first-listed are higher priority): +==== Overrides -- Program arguments -- System properties -- Environment variables -- Environment property file -- Property file +Properties are resolved using the following precedence (highest priority first): -.application.conf -[source, properties] ----- -foo = foo ----- +1. Program arguments (e.g., `java -jar app.jar foo=bar`) +2. System properties (e.g., `-Dfoo=bar`) +3. Environment variables (e.g., `foo=bar java -jar app.jar`) +4. Environment-specific property file (e.g., `application.prod.conf`) +5. Default property file (`application.conf`) -.Property access +.Accessing Properties [source, java, role="primary"] ---- { - Environment env = getEnvironment(); <1> - Config conf = env.getConfig(); <2> - System.out.println(conf.getString("foo")); <3> + Environment env = getEnvironment(); // <1> + Config conf = env.getConfig(); // <2> + System.out.println(conf.getString("foo")); // <3> } ---- @@ -115,101 +69,41 @@ foo = foo [source, kotlin, role="secondary"] ---- { - val env = environment <1> - val conf = env.config <2> - println(conf.getString("foo")) <3> + val env = environment // <1> + val conf = env.config // <2> + println(conf.getString("foo")) // <3> } ---- -<1> Get environment -<2> Get configuration -<3> Get `foo` property and prints `foo` - -At runtime you can override properties using: - -.Program argument -[source, bash] ----- -java -jar myapp.jar foo=argument ----- - -Example prints: `argument` - -.System property -[source, bash] ----- -java -Dfoo=sysprop -jar myapp.jar ----- - -Prints: `syspro` +<1> Retrieve the current environment. +<2> Access the underlying `Config` object. +<3> Extract the value for the key `foo`. -.Environment variable -[source, bash] ----- -foo=envar java -jar myapp.jar ----- - -Prints: `envar` +==== Multi-Environment Configuration -If you have multiple properties to override, it is probably better to collect all them into a new file -and use active environment name to select them. +It is best practice to keep common settings in `application.conf` and override environment-specific values in separate files named `application.[env].conf`. -.Environment property file -[source, bash] +.Example Structure ---- -└── application.conf -└── application.prod.conf +└── application.conf (foo = "default", bar = "base") +└── application.prod.conf (foo = "production") ---- -.application.conf -[source, properties] ----- -foo = foo -bar = devbar ----- +Running with `java -jar myapp.jar prod` results in: +* `foo`: `"production"` (overridden) +* `bar`: `"base"` (inherited from default) -.application.prod.conf -[source, properties] ----- -bar = prodbar ----- +To activate multiple environments, separate them with commas: `java -jar app.jar prod,cloud`. -.Run with `prod` environment ----- -java -jar my.app application.env=prod ----- +==== Custom Configuration -Or just ----- -java -jar my.app prod ----- +If you want to bypass Jooby's default loading logic, you can provide custom options or instantiate the environment manually. -TIP: You only need to override the properties that changes between environment not all the properties. - -The `application.conf` defines two properties : `foo` and `bar`, while the environment property file -defines only `bar`. - -For Multiple environment activation you need to separate them with `,` (comma): - -.Run with `prod` and `cloud` environment ----- - java -jar my.app application.env=prod,cloud ----- - -=== Custom environment - -Custom configuration and environment are available too using: - -- The javadoc:EnvironmentOptions[] class, or -- Direct instantiation of the javadoc:Environment[] class - -.Environment options +.Using Environment Options [source,java,role="primary"] ---- { - Environment env = setEnvironmentOptions( - new EnvironmentOptions().setFilename("myapp.conf")); <1> - ) + setEnvironmentOptions(new EnvironmentOptions().setFilename("myapp.conf")); // <1> } ---- @@ -217,26 +111,21 @@ Custom configuration and environment are available too using: [source,kotlin,role="secondary"] ---- { - val env = environmentOptions { <1> - filename = "myapp.conf" + environmentOptions { + filename = "myapp.conf" // <1> } } ---- -<1> Load `myapp.conf` using the loading and precedence mechanism described before - -The javadoc:Jooby[setEnvironmentOptions, io.jooby.EnvironmentOptions] method loads, set and returns -the environment. - -To skip/ignore Jooby loading and precedence mechanism, just instantiate and set the environment: +<1> Loads `myapp.conf` instead of the default `application.conf` while maintaining standard precedence rules. -.Direct instantiation +.Direct Instantiation [source,java,role="primary"] ---- { - Config conf = ConfigFactory.load("myapp.conf"); <1> - Environment env = new Environment(getClassLoader(), conf); <2> - setEnvironment(env); <3> + Config conf = ConfigFactory.load("custom.conf"); // <1> + Environment env = new Environment(getClassLoader(), conf); // <2> + setEnvironment(env); // <3> } ---- @@ -244,149 +133,49 @@ To skip/ignore Jooby loading and precedence mechanism, just instantiate and set [source,kotlin,role="secondary"] ---- { - val conf = ConfigFactory.load("myapp.conf") <1> - val env = Environment(classLoader, conf) <2> - environment = env <3> + val conf = ConfigFactory.load("custom.conf") // <1> + val env = Environment(classLoader, conf) // <2> + environment = env // <3> } ---- -<1> Loads and parses configuration -<2> Create a new environment with configuration and (optionally) active names -<3> Set environment on Jooby instance +<1> Manually load a configuration file. +<2> Wrap it in a Jooby Environment. +<3> Assign it to the application before startup. -IMPORTANT: Custom configuration is very flexible. You can reuse Jooby mechanism or provide your own. -The only thing to keep in mind is that environment setting must be done at very early stage, before -starting the application. +==== Logging -=== Logging +Jooby uses **SLF4J**, allowing you to plug in your preferred logging framework. -Jooby uses https://www.slf4j.org[Slf4j] for logging which give you some flexibility for choosing -the logging framework. +===== Logback -==== Logback +1. **Add Dependency:** `logback-classic`. +2. **Configure:** Place `logback.xml` in your `conf` directory or classpath root. -The https://logback.qos.ch/manual/index.html[Logback] is probably the first alternative for -https://www.slf4j.org[Slf4j] due its natively implements the SLF4J API. Follow the next steps to use -logback in your project: +===== Log4j2 -1) Add dependency +1. **Add Dependencies:** `log4j-slf4j-impl` and `log4j-core`. +2. **Configure:** Place `log4j2.xml` in your `conf` directory or classpath root. -[dependency, artifactId="logback-classic"] +===== Environment-Aware Logging -2) Creates a `logback.xml` file in the `conf` directory: - -.logback.xml -[source, xml] ----- - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - ----- - -That's all! https://www.slf4j.org[Slf4j] is going to redirect log message to logback. - -==== Log4j2 - -The https://logging.apache.org/log4j[Log4j2] project is another good alternative for logging. Follow -the next steps to use logback in your project: - -1) Add dependencies - -[dependency, artifactId="log4j-slf4j-impl, log4j-core"] - -2) Creates a `log4j.xml` file in the `conf` directory: - -.log4j.xml -[source, xml] ----- - - - - - - - - - - - - - ----- - -All these extensions are considered valid: `.xml`, `.propertines`, `.yaml` and `.json`. As well as `log4j2` for file name. - -==== Environment logging - -Logging is integrated with the environment names. So it is possible to have a file name: - -- `logback[.name].xml` (for loggback) -- `log4j[.name].xml` (for log4j2) - -Jooby favors the environment specific logging configuration file over regular/normal logging configuration file. - -.Example -[source, bash] ----- -conf -└── logback.conf -└── logback.prod.conf ----- - -To use `logback.prod.conf`, start your application like: - -`java -jar myapp.jar application.env=prod` +Logging is also environment-aware. Jooby will look for `logback.[env].xml` or `log4j2.[env].xml` and favor them over the default files. [IMPORTANT] ==== -The logging configuration file per environment works as long you don't use *static* loggers -before application has been start it. The next example won't work: - -[source, java] ----- -public class App extends Jooby { - private static final Logger log = ... - - public static void main(String[] args) { - runApp(args, App::new); - } -} ----- - -The `runApp` method is the one who configures the logging framework. Adding a static logger force -the logging framework to configure without taking care the environment setup. - -There are a couple of solution is for this: - -- use an instance logger -- use the getLog() method of Jooby +To ensure environment-specific logging works correctly, avoid using **static** loggers in your main App class before `runApp` is called. Static loggers force the logging framework to initialize before Jooby can apply the environment-specific configuration. Use an instance logger or Jooby's `getLog()` method instead. ==== -=== Application Properties +==== Application Properties -These are the application properties that Jooby uses: - -[options="header"] |=== -|Property name | Description | Default value -|application.charset | Charset used by your application. Used by template engine, HTTP encoding/decoding, database driver, etc. | `UTF-8` -|application.env | The active <> names. Use to identify `dev` vs `non-dev` application deployment. Jooby applies some optimizations for `non-dev`environments | `dev` -|application.lang | The languages your application supports. Used by `Context.locale()` | A single locale provided by `Locale.getDefault()`. -|application.logfile | The logging configuration file your application uses. You don't need to set this property, see <>. | -|application.package | The base package of your application. | -|application.pid | JVM process ID. | The native process ID assigned by the operating system. -|application.startupSummary | The level of information logged during startup. | -|application.tmpdir | Temporary directory used by your application. | `tmp` +|Property | Description | Default + +|`application.charset` | Charset for encoding/decoding and templates. | `UTF-8` +|`application.env` | Active environment names. Jooby optimizes performance for non-`dev` environments. | `dev` +|`application.lang` | Supported languages for `Context.locale()`. | `Locale.getDefault()` +|`application.tmpdir` | Temporary directory for the application. | `tmp` +|`application.pid` | The JVM process ID. | System assigned |=== -See javadoc:AvailableSettings[] for more details. +See javadoc:AvailableSettings[] for a complete reference. diff --git a/docs/asciidoc/context.adoc b/docs/asciidoc/context.adoc index 47f5921248..45a12a64a6 100644 --- a/docs/asciidoc/context.adoc +++ b/docs/asciidoc/context.adoc @@ -1,14 +1,14 @@ -== Context +=== Context -A javadoc:Context[Context] allows you to interact with the HTTP Request and manipulate the HTTP Response. +A javadoc:Context[Context] allows you to interact with the HTTP request and manipulate the HTTP response. -In most of the cases you can access the context object as a parameter of your route handler: +In most cases, you access the context object as a parameter of your route handler: .Java [source, java, role="primary"] ---- { - get("/", ctx -> { /* do important stuff with variable 'ctx'. */ }); + get("/", ctx -> { /* do important stuff with the 'ctx' variable */ }); } ---- @@ -16,23 +16,15 @@ In most of the cases you can access the context object as a parameter of your ro [source, kotlin, role="secondary"] ---- { - get("/") { /* variable 'it' holds the context now. */ } + get("/") { /* the 'it' variable (or implicit ctx) holds the context */ } } ---- -javadoc:Context[Context] also provides derived information about the current request such as a -matching locale (or locales) based on the `Accept-Language` header (if presents). You may use -the result of javadoc:Context[locale] or javadoc:Context[locales] to present content matching to -the user's language preference. +javadoc:Context[Context] also provides derived information about the current request, such as matching locales based on the `Accept-Language` header. You can use javadoc:Context[locale] or javadoc:Context[locales] to present content matching the user's language preference. -The above methods use `Locale.lookup(...)` and `Locale.filter(...)` respectively to perform the -language tag matching. See their overloads if you need to plug in your own matching strategy. +These methods use `Locale.lookup(...)` and `Locale.filter(...)` to perform language tag matching. (See their overloads if you need to plug in a custom matching strategy). -To leverage language matching however, you need to tell Jooby which languages your application -supports. This can be done by either setting the `application.lang` configuration property -to a value compatible with the -https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[Accept-Language] -header: +To leverage language matching, you must tell Jooby which languages your application supports. Set the `application.lang` configuration property to a value compatible with the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[Accept-Language] header: .application.conf [source, properties] @@ -40,8 +32,7 @@ header: application.lang = en, en-GB, de ---- -or calling the javadoc:Jooby[setLocales, java.util.List] -or javadoc:Jooby[setLocales, java.util.Locale...] method at runtime: +Or configure it programmatically using javadoc:Jooby[setLocales, java.util.List]: .Java [source, java, role="primary"] @@ -59,24 +50,17 @@ or javadoc:Jooby[setLocales, java.util.Locale...] method at runtime: } ---- -If you don't set the supported locales explicitly, Jooby uses a single locale provided by -`Locale.getDefault()`. +If you don't explicitly set the supported locales, Jooby falls back to a single locale provided by `Locale.getDefault()`. -=== Parameters +==== Parameters -There are several parameter types: `header`, `cookie`, `path`, `query`, `form`, `multipart`, -`session` and `flash`. All them share a unified/type-safe API for accessing and manipulating their values. +There are several parameter types: `header`, `cookie`, `path`, `query`, `form`, `multipart`, `session`, and `flash`. They all share a unified, type-safe API for accessing and manipulating their values. -We are going to describe them briefly in the next sections, then go into specific features of the -<>. +This section covers how to extract raw parameters. The next section covers how to convert them into complex objects using the <>. -There is also a <> feature by which you can access a parameter from any combination -of the above types with well-defined priority. +===== Header -==== Header - -HTTP headers allow the client and the server to pass additional information with the request or the -response. +HTTP headers allow the client and server to pass additional information. .Java [source, java, role="primary"] @@ -84,13 +68,10 @@ response. { get("/", ctx -> { String token = ctx.header("token").value(); // <1> - Value headers = ctx.headers(); // <2> - Map headerMap = ctx.headerMap(); // <3> - ... + // ... }); - } ---- @@ -100,24 +81,20 @@ response. { get("/") { ctx -> val token = ctx.header("token").value() // <1> - val headers = ctx.headers() // <2> - - val headerMap = ctx.headerMap(); // <3> - ... + val headerMap = ctx.headerMap() // <3> + // ... } - } ---- -<1> Header variable `token` -<2> All headers as javadoc:value.Value[] -<3> All headers as map +<1> Retrieves the header `token`. +<2> Retrieves all headers as a javadoc:value.Value[]. +<3> Retrieves all headers as a Map. -==== Cookie +===== Cookie -Request cookies are send to the server using the `Cookie` header, but we do provide a simple -`key/value` access to them: +Request cookies are sent to the server via the `Cookie` header. Jooby provides simple key/value access: .Cookies [source, java, role="primary"] @@ -125,11 +102,9 @@ Request cookies are send to the server using the `Cookie` header, but we do prov { get("/", ctx -> { String token = ctx.cookie("token").value(); // <1> - Map cookieMap = ctx.cookieMap(); // <2> - ... + // ... }); - } ---- @@ -139,34 +114,28 @@ Request cookies are send to the server using the `Cookie` header, but we do prov { get("/") { ctx -> val token = ctx.cookie("token").value() // <1> - val cookieMap = ctx.cookieMap() // <2> - ... + // ... } - } ---- -<1> Cookie variable `token` -<2> All cookies as map +<1> Retrieves the cookie named `token`. +<2> Retrieves all cookies as a Map. -==== Path +===== Path -Path parameter are part of the `URI`. To define a path variable you need to use the `{identifier}` notation. +Path parameters are part of the URI. Use the `{identifier}` notation to define a path variable. .Syntax: [source,java,role="primary"] ---- { - get("/{id}" ctx -> ctx.path("id").value()); // <1> - - get("/@{id}" ctx -> ctx.path("id").value()); // <2> - - get("/file/{name}.{ext}", ctx -> cxt.path("name") + "." + ctx.path("ext")); // <3> - - get("/file/*", ctx -> ctx.path("*")) // <4> - - get("/{id:[0-9]+}", ctx -> ctx.path("id)) // <5> + get("/{id}", ctx -> ctx.path("id").value()); // <1> + get("/@{id}", ctx -> ctx.path("id").value()); // <2> + get("/file/{name}.{ext}", ctx -> ctx.path("name") + "." + ctx.path("ext")); // <3> + get("/file/*", ctx -> ctx.path("*")); // <4> + get("/{id:[0-9]+}", ctx -> ctx.path("id")); // <5> } ---- @@ -175,37 +144,29 @@ Path parameter are part of the `URI`. To define a path variable you need to use ---- { get("/{id}") { ctx -> ctx.path("id").value() } // <1> - get("/@{id}") { ctx -> ctx.path("id").value() } // <2> - - get("/file/{name}.{ext}") { ctx -> cxt.path("name") + "." + ctx.path("ext") } // <3> - + get("/file/{name}.{ext}") { ctx -> ctx.path("name") + "." + ctx.path("ext") } // <3> get("/file/*") { ctx -> ctx.path("*") } // <4> - - get("/{id:[0-9]+}") { ctx -> ctx.path("id) } // <5> + get("/{id:[0-9]+}") { ctx -> ctx.path("id") } // <5> } ---- -<1> Path variable `id` -<2> Path variable `id` prefixed with `@` -<3> Multiple variables `name` and `ext` -<4> Unnamed catchall path variable -<5> Path variable with a regular expression +<1> Standard path variable `id`. +<2> Path variable `id` prefixed with `@`. +<3> Multiple variables: `name` and `ext`. +<4> Unnamed catchall path variable. +<5> Path variable strictly matching a regular expression. -.Java +.Accessing Path Variables [source, java, role="primary"] ---- { get("/{name}", ctx -> { String pathString = ctx.getRequestPath(); // <1> - Value path = ctx.path(); // <2> - Map pathMap = ctx.pathMap(); // <3> - String name = ctx.path("name").value(); // <4> - - ... + // ... }); } ---- @@ -216,45 +177,22 @@ Path parameter are part of the `URI`. To define a path variable you need to use { get("/{name}") { ctx -> val pathString = ctx.getRequestPath() // <1> - val path = ctx.path() // <2> - val pathMap = ctx.pathMap() // <3> - val name = ctx.path("name").value() // <4> - - ... + // ... } } ---- -<1> Access to the `raw` path string: - -- `/a+b` => `/a+b` -- `/a b` => `/a%20b` (not decoded) -- `/%2F%2B` => `/%2F%2B` (not decoded) - -<2> Path as javadoc:value.Value[] object: - -- `/a+b` => `{name=a+b}` -- `/a b` => `{name=a b}` (decoded) -- `/%2F%2B` => `{name=/+}` (decoded) - -<3> Path as `Map` object: - -- `/a+b` => `{name=a+b}` -- `/a b` => `{name=a b}` (decoded) -- `/%2F%2B` => `{name=/+}` (decoded) +<1> Access the `raw` path string (e.g., `/a b` returns `/a%20b`). +<2> Path as a javadoc:value.Value[] object (decoded). +<3> Path as a `Map` (decoded). +<4> Specific path variable `name` as a `String` (decoded). -<4> Path variable `name` as `String`: +===== Query -- `/a+b` => `a+b` -- `/a b` => `a b` (decoded) -- `/%2F%2B` => `/+` (decoded) - -==== Query - -Query String is part of the `URI` that start after the `?` character. +The query string is the part of the URI that starts after the `?` character. .Java [source, java, role="primary"] @@ -262,26 +200,17 @@ Query String is part of the `URI` that start after the `?` character. { get("/search", ctx -> { String queryString = ctx.queryString(); // <1> - QueryString query = ctx.query(); // <2> - Map> queryMap = ctx.queryMultimap(); // <3> - String q = ctx.query("q").value(); // <4> - SearchQuery searchQuery = ctx.query(SearchQuery.class); // <5> - - ... + // ... }); } class SearchQuery { - public final String q; - - public SearchQuery(String q) { - this.q = q; - } + public SearchQuery(String q) { this.q = q; } } ---- @@ -291,57 +220,26 @@ class SearchQuery { { get("/search") { ctx -> val queryString = ctx.queryString() // <1> - val query = ctx.query() // <2> - val queryMap = ctx.queryMultimap() // <3> - val q = ctx.query("q").value() // <4> - val searchQuery = ctx.query() // <5> - - ... + // ... } } -data class SearchQuery (val q: String) +data class SearchQuery(val q: String) ---- -<1> Access to `raw` queryString: - -- `/search` => `""` (empty) -- `/search?q=a+b` => `?q=a+b` -- `/search?q=a b` => `?q=a%20b` (not decoded) +<1> Access the `raw` query string (e.g., `?q=a%20b`). +<2> Query string as a javadoc:QueryString[] object (e.g., `{q=a b}`). +<3> Query string as a multi-value map (e.g., `{q=[a b]}`). +<4> Access decoded variable `q`. Throws a `400 Bad Request` if missing. +<5> Binds the query string directly to a `SearchQuery` object. -<2> Query String as javadoc:QueryString[] object: +===== Formdata -- `/search` => `{}` (empty) -- `/search?q=a+b` => `{q=a+b}` -- `/search?q=a b` => `{q=a b}` (decoded) - -<3> Query string as `multi-value map` - -- `/search` => `{}` (empty) -- `/search?q=a+b` => `{q=[a+b]}` -- `/search?q=a b` => `{q=[a b]}` (decoded) - -<4> Access to decoded variable `q`: - -- `/search` => `Bad Request (400). Missing value: "q"` -- `/search?q=a+b` => `a+b` -- `/search?q=a b` => `a b` (decoded) - -<5> Query string as `SearchQuery` - -- `/search` => `Bad Request (400). Missing value: "q"` -- `/search?q=a+b` => `SearchQuery(q="a+b")` -- `/search?q=a b` => `SearchQuery(q="a b")` (decoded) - -==== Formdata - -Formdata is expected to be in HTTP body, or for as part of the `URI` for `GET` requests. - -Data is expected to be encoded as `application/x-www-form-urlencoded`. +Form data is sent in the HTTP body (or as part of the URI for `GET` requests) and is encoded as `application/x-www-form-urlencoded`. .Java [source, java, role="primary"] @@ -349,25 +247,17 @@ Data is expected to be encoded as `application/x-www-form-urlencoded`. { post("/user", ctx -> { Formdata form = ctx.form(); // <1> - Map> formMap = ctx.formMultimap(); // <2> - String userId = ctx.form("id").value(); // <3> - String pass = ctx.form("pass").value(); // <4> - User user = ctx.form(User.class); // <5> - - ... + // ... }); } class User { - public final String id; - public final String pass; - public User(String id, String pass) { this.id = id; this.pass = pass; @@ -381,36 +271,26 @@ class User { { post("/user") { ctx -> val form = ctx.form() // <1> - val formMap = ctx.formMultimap() // <2> - val userId = ctx.form("id").value() // <3> - val pass = ctx.form("pass").value() // <4> - val user = ctx.form() // <5> - - ... + // ... } } -data class User (val id: String, val pass: String) - +data class User(val id: String, val pass: String) ---- ----- -curl -d "id=root&pass=pwd" -X POST http://localhost:8080/user ----- +<1> Form as javadoc:Formdata[]. +<2> Form as a multi-value map. +<3> Specific form variable `id`. +<4> Specific form variable `pass`. +<5> Form automatically bound to a `User` object. -<1> Form as javadoc:Formdata[] => `{id=root, pass=pwd}` -<2> Form as `multi-value map` => `{id=root, pass=[pwd]}` -<3> Form variable `id` => `root` -<4> Form variable `pass` => `pwd` -<5> Form as `User` object => `User(id=root, pass=pwd)` +===== Multipart & File Uploads -==== Multipart - -Form-data must be present in the HTTP body and encoded as `multipart/form-data`: +Multipart data is sent in the HTTP body and encoded as `multipart/form-data`. It is required for file uploads. .Java [source, java, role="primary"] @@ -418,29 +298,19 @@ Form-data must be present in the HTTP body and encoded as `multipart/form-data`: { post("/user", ctx -> { Multipart multipart = ctx.multipart(); // <1> - - Map multipartMap = ctx.multipartMultimap(); // <2> - + Map> multipartMap = ctx.multipartMultimap(); // <2> String userId = ctx.multipart("id").value(); // <3> - String pass = ctx.multipart("pass").value(); // <4> - FileUpload pic = ctx.file("pic"); // <5> - User user = ctx.multipart(User.class); // <6> - - ... + // ... }); } class User { - public final String id; - public final String pass; - public final FileUpload pic; - public User(String id, String pass, FileUpload pic) { this.id = id; this.pass = pass; @@ -455,48 +325,35 @@ class User { { post("/user") { ctx -> val multipart = ctx.multipart() // <1> - val multipartMap = ctx.multipartMultimap() // <2> - val userId = ctx.multipart("id").value() // <3> - val pass = ctx.multipart("pass").value() // <4> - val pic = ctx.file("pic") // <5> - val user = ctx.multipart() // <6> - - ... + // ... } } -data class User (val id: String, val pass: String, val pic: FileUpload) ----- - ----- -curl -F id=root -F pass=root -F pic=@/path/to/local/file/profile.png http://localhost:8080/user +data class User(val id: String, val pass: String, val pic: FileUpload) ---- -<1> Form as javadoc:Multipart[] => `{id=root, pass=pwd, pic=profile.png}` -<2> Form as `multi-value map` => `{id=root, pass=[pwd]}` -<3> Form variable `id` => `root` -<4> Form variable `pass` => `pwd` -<5> javadoc:FileUpload[] variable `pic` -<6> Form as `User` object => `User(id=root, pass=pwd, pic=profile.png)` +<1> Form as javadoc:Multipart[]. +<2> Form as a multi-value map. +<3> Specific multipart text variable `id`. +<4> Specific multipart text variable `pass`. +<5> Single file upload named `pic`. +<6> Multipart form bound to a `User` object (including the file). [NOTE] .File Upload ==== - -File upload are available ONLY for multipart requests. +File uploads are **only** available for multipart requests. .Java [source,java,role="primary"] ---- FileUpload pic = ctx.file("pic"); // <1> - - List pic = ctx.files("pic"); // <2> - + List pics = ctx.files("pic"); // <2> List files = ctx.files(); // <3> ---- @@ -504,58 +361,50 @@ File upload are available ONLY for multipart requests. [source,kotlin,role="secondary"] ---- val pic = ctx.file("pic") // <1> - - val pic = ctx.files("pic") // <2> - + val pics = ctx.files("pic") // <2> val files = ctx.files() // <3> ---- <1> Single file upload named `pic` <2> Multiple file uploads named `pic` <3> All file uploads - ==== -==== Session +===== Session -Session parameters are available via javadoc:Context[session] or javadoc:Context[sessionOrNull] -methods. HTTP Session is covered in his own <>, but here is a quick look: +Session parameters are available via javadoc:Context[session] or javadoc:Context[sessionOrNull]. (See the full <> for details). .Java [source,java,role="primary"] ---- Session session = ctx.session(); // <1> - String attribute = ctx.session("attribute").value(); // <2> - ---- .Kotlin [source,kotlin,role="secondary"] ---- val session = ctx.session() // <1> - val attribute = session.attribute("attribute").value() // <2> ---- -<1> Find an existing Session or create one -<2> Get a session attribute +<1> Finds an existing Session or creates a new one. +<2> Gets a specific session attribute. -==== Flash +===== Flash -Flash parameters are designed to transport success/error messages between requests. It is similar to -a javadoc:Session[] but the lifecycle is shorter: *data is kept for only one request*. +Flash parameters transport success/error messages between requests. They are similar to a session, but their lifecycle is shorter: **data is kept for only one request**. .Java [source,java,role="primary"] ---- get("/", ctx -> { - return ctx.flash("success").value("Welcome!"); <3> + return ctx.flash("success").value("Welcome!"); // <3> }); post("/save", ctx -> { - ctx.flash().put("success", "Item created"); <1> - return ctx.sendRedirect("/"); <2> + ctx.flash().put("success", "Item created"); // <1> + return ctx.sendRedirect("/"); // <2> }); ---- @@ -563,28 +412,26 @@ a javadoc:Session[] but the lifecycle is shorter: *data is kept for only one req [source,kotlin,role="secondary"] ---- get("/") { ctx -> - ctx.flash("success").value("Welcome!") <3> + ctx.flash("success").value("Welcome!") // <3> } post("/save") { ctx -> - ctx.flash().put("success", "Item created") <1> - ctx.sendRedirect("/") <2> + ctx.flash().put("success", "Item created") // <1> + ctx.sendRedirect("/") // <2> } ---- -<1> Set a flash attribute: `success` -<2> Redirect to home page -<3> Display an existing flash attribute `success` or shows `Welcome!` +<1> Sets a flash attribute: `success`. +<2> Redirects to the home page. +<3> Displays the flash attribute `success` (if it exists) or defaults to `Welcome!`. -Flash attributes are implemented using an `HTTP Cookie`. To customize the cookie -(its name defaults to `jooby.flash`) use the javadoc:Router[setFlashCookie, io.jooby.Cookie] method: +Flash attributes are implemented using an HTTP Cookie. To customize the cookie (the default name is `jooby.flash`), use javadoc:Router[setFlashCookie, io.jooby.Cookie]. .Java [source,java,role="primary"] ---- { setFlashCookie(new Cookie("myflash").setHttpOnly(true)); - // or if you're fine with the default name getFlashCookie().setHttpOnly(true); } @@ -595,16 +442,14 @@ Flash attributes are implemented using an `HTTP Cookie`. To customize the cookie ---- { flashCookie = Cookie("myflash").setHttpOnly(true) - // or if you're fine with the default name flashCookie.isHttpOnly = true } ---- -==== Parameter Lookup +===== Parameter Lookup -You can search for parameters in multiple sources with an explicitly defined priority using the -javadoc:Context[lookup] or javadoc:Context[lookup, java.lang.String, io.jooby.ParamSource...] method: +You can search for parameters across multiple sources with an explicitly defined priority using javadoc:Context[lookup]. .Java [source,java,role="primary"] @@ -634,21 +479,18 @@ get("/{foo}") { ctx -> } ---- -In case of a request like `/bar?foo=baz`, `foo is: baz` will be returned since the query parameter -takes precedence over the path parameter. +If a request is made to `/bar?foo=baz`, the result will be `foo is: baz` because the query parameter takes precedence over the path parameter. -==== Client Certificates +===== Client Certificates -If mutual TLS is enabled, you can access the client's certificates from the context. The first -certificate in the list is the peer certificate, followed by the ca certificates in the chain -(the order is preserved). +If mutual TLS is enabled, you can access the client's certificates from the context. The first certificate in the list is the peer certificate, followed by the CA certificates in the chain. .Java [source,java,role="primary"] ---- get("/{foo}", ctx -> { - List certificates = ctx.getClientCertificates(); <1> - Certificate peerCertificate = certificates.get(0); <2> + List certificates = ctx.getClientCertificates(); // <1> + Certificate peerCertificate = certificates.get(0); // <2> }); ---- @@ -656,12 +498,12 @@ get("/{foo}", ctx -> { [source,kotlin,role="secondary"] ---- get("/{foo}") { ctx -> - val certificates = ctx.clientCertificates <1> - val peerCertificate = certificates.first() <2> + val certificates = ctx.clientCertificates // <1> + val peerCertificate = certificates.first() // <2> } ---- -<1> Get all of the certificates presented by the client during the SSL handshake. +<1> Get all certificates presented by the client during the SSL handshake. <2> Get only the peer certificate. include::value-api.adoc[] diff --git a/docs/asciidoc/core.adoc b/docs/asciidoc/core.adoc new file mode 100644 index 0000000000..68e2681b84 --- /dev/null +++ b/docs/asciidoc/core.adoc @@ -0,0 +1,13 @@ +== Core + +include::routing.adoc[] + +include::context.adoc[] + +include::responses.adoc[] + +include::handlers.adoc[] + +include::error-handler.adoc[] + +include::execution-model.adoc[] diff --git a/docs/asciidoc/dev-tools.adoc b/docs/asciidoc/dev-tools.adoc index 8fc01fa288..88d39c8038 100644 --- a/docs/asciidoc/dev-tools.adoc +++ b/docs/asciidoc/dev-tools.adoc @@ -1,49 +1,35 @@ -== Development +=== Development -The `jooby run` tool allows to restart your application on code changes without exiting the JVM. +The `jooby run` tool provides a "hot reload" experience by restarting your application automatically whenever code changes are detected, without exiting the JVM. This makes Java and Kotlin development feel as fast and iterative as a scripting language. -This feature is also known as hot reload/swap. Makes you feel like coding against a script -language where you modify your code and changes are visible immediately. - -The tool uses the https://jboss-modules.github.io/jboss-modules/manual[JBoss Modules] library -that effectively reload application classes. +The tool leverages https://jboss-modules.github.io/jboss-modules/manual[JBoss Modules] to efficiently reload application classes. It is available as both a Maven and a Gradle plugin. -For now `jooby run` is available as Maven and Gradle plugins. +==== Usage -=== Usage - -1) Add build plugin: +1) Add the build plugin: .pom.xml [source, xml, role = "primary", subs="verbatim,attributes"] ---- - ... io.jooby jooby-maven-plugin {joobyVersion} - ... ---- .build.gradle [source, gradle, role = "secondary", subs="verbatim,attributes"] ---- -buildscript { - ext { - joobyVersion = "{joobyVersion}" - } -} - plugins { id "application" id "io.jooby.run" version "{joobyVersion}" } ---- -2) Set main class +2) Configure the Main Class: .pom.xml [source, xml, role = "primary"] @@ -59,7 +45,7 @@ plugins { mainClassName = "myapp.App" ---- -3) Run application +3) Launch the Application: .Maven [source, bash, role = "primary"] @@ -73,77 +59,57 @@ mvn jooby:run ./gradlew joobyRun ---- -=== Compilation & Restart - -Changing a `java` or `kt` file triggers a compilation request. Compilation is executed by -Maven/Gradle using an incremental build process. - -If compilation succeed, application is restarted. - -Compilation errors are printed to the console by Maven/Gradle. +==== Compilation and Restart -Changing a `.conf`, `.properties` file triggers just an application restart request. They don't trigger -a compilation request. +* **Source Files (`.java`, `.kt`):** Changing a source file triggers an incremental compilation request. If the compilation succeeds, the application restarts automatically. +* **Configuration Files (`.conf`, `.properties`):** Changes to these files trigger an immediate application restart without a compilation step. +* **Compilation Errors:** Any errors during the build process are printed directly to the console by Maven or Gradle. -Compiler is enabled by default, except for Eclipse users. Plugin checks for `.classpath` file in -project directory, when found plugin compiler is OFF and let Eclipse compiles the code. +[NOTE] +==== +For **Eclipse** users: The plugin detects the `.classpath` file in your project directory. If found, the plugin's internal compiler is disabled, letting Eclipse handle the compilation while the plugin focuses on the restart logic. +==== -=== Options +==== Options -The next example shows all the available options with their default values: +Below are the available configuration options with their default values: .pom.xml [source, xml, role = "primary", subs="verbatim,attributes"] ---- - - ... - - io.jooby - jooby-maven-plugin - {joobyVersion} - - ${application.class} <1> - conf,properties,class <2> - java,kt <3> - 8080 <4> - 500 <5> - false <6> - - - ... - + + ${application.class} + conf,properties,class + java,kt + 8080 + 500 + false + ---- .build.gradle [source, gradle, role = "secondary", subs="verbatim,attributes"] ---- -buildscript { - ext { - joobyVersion = "{joobyVersion}" - } -} - -plugins { - id "application" - id "io.jooby.run" version "{joobyVersion}" -} - joobyRun { - mainClassName = "${mainClassName}" <1> - restartExtensions = ["conf", "properties", "class"] <2> - compileExtensions = ["java", "kt"] <3> - port = 8080 <4> - waitTimeBeforeRestart = 500 <5> - useSingleClassLoader = false <6> + mainClassName = "${mainClassName}" // <1> + restartExtensions = ["conf", "properties", "class"] // <2> + compileExtensions = ["java", "kt"] // <3> + port = 8080 // <4> + waitTimeBeforeRestart = 500 // <5> + useSingleClassLoader = false // <6> } ---- -<1> Application main class -<2> Restart extensions. A change on these files trigger a restart request. -<3> Source extensions. A change on these files trigger a compilation request, followed by a restart request. -<4> Application port -<5> How long to wait after last file change to restart. Default is: `500` milliseconds. -<6> Use a single/fat class loader to run your application. This is required on complex project classpath where you start seeing weird reflection errors. This was the default mode in Jooby 2.x. The new model since 3.x uses a modular classloader which improves restart times and memory usage making it faster. Default is: `false`. +<1> The application's entry point (Main class). +<2> Extensions that trigger an immediate restart. +<3> Extensions that trigger a compilation followed by a restart. +<4> The local development port. +<5> The delay (in milliseconds) to wait after the last file change before restarting. Default is `500ms`. +<6> If `true`, Jooby uses a single "fat" classloader. Set this to `true` if you encounter strange reflection or class-loading errors in complex projects. Since 3.x, Jooby uses a modular classloader by default for faster restarts and lower memory usage. + +==== Testing with Classpath + +To run the application while including the `test` scope/source set in the classpath, use the following commands: -For Maven and Gradle there are two variant `mvn jooby:testRun` and `./gradlew joobyTestRun` they work -by expanding the classpath to uses the `test` scope or source set. +* **Maven:** `mvn jooby:testRun` +* **Gradle:** `./gradlew joobyTestRun` diff --git a/docs/asciidoc/docinfo-footer.html b/docs/asciidoc/docinfo-footer.html index 52f25902ee..0305aa8497 100644 --- a/docs/asciidoc/docinfo-footer.html +++ b/docs/asciidoc/docinfo-footer.html @@ -12,43 +12,7 @@ }); }); - - - - + + + + - - - diff --git a/docs/asciidoc/docinfo-header.html b/docs/asciidoc/docinfo-header.html new file mode 100644 index 0000000000..9c5e1f744f --- /dev/null +++ b/docs/asciidoc/docinfo-header.html @@ -0,0 +1,6 @@ +

+ Jooby + +
diff --git a/docs/asciidoc/docinfo.html b/docs/asciidoc/docinfo.html index 2e8b60e0d3..27704b6ebf 100644 --- a/docs/asciidoc/docinfo.html +++ b/docs/asciidoc/docinfo.html @@ -1,109 +1,12 @@ - - + + + + diff --git a/docs/asciidoc/ecosystem.adoc b/docs/asciidoc/ecosystem.adoc index 38972786ad..36359f1679 100644 --- a/docs/asciidoc/ecosystem.adoc +++ b/docs/asciidoc/ecosystem.adoc @@ -1,4 +1,6 @@ == Ecosystem +[.lead] +Extend the power of Jooby through its rich ecosystem of modules and standards. Learn how to seamlessly integrate with OpenAPI 3 to automatically generate interactive documentation and client SDKs, and explore a wide array of community and first-party modules that bring database access, security, and messaging to your application with minimal configuration. The Jooby ecosystem is built on three core, interconnected concepts: @@ -131,7 +133,7 @@ public class MyExtension implements Extension { public void install(Jooby app) { DataSource dataSource = createDataSource(); // <1> - app.getServices().put(DataSource.class, dataSource); // <2> + app.getServices().put(DataSource.class, dataSource);// <2> app.onStop(dataSource::close); // <3> } @@ -150,11 +152,11 @@ import io.jooby.Jooby class MyExtension : Extension { override fun install(app: Jooby) { - val dataSource = createDataSource() // <1> + val dataSource = createDataSource() // <1> - app.services.put(DataSource::class.java, dataSource) // <2> + app.services.put(DataSource::class.java, dataSource) // <2> - app.onStop(dataSource::close) // <3> + app.onStop(dataSource::close) // <3> } private fun createDataSource(): DataSource { diff --git a/docs/asciidoc/index.adoc b/docs/asciidoc/index.adoc index d76b167d8c..518d583ee1 100644 --- a/docs/asciidoc/index.adoc +++ b/docs/asciidoc/index.adoc @@ -19,7 +19,7 @@ Style guidelines: [discrete] == ∞ do more, more easily -Jooby is a modern, fast, and easy-to-use web framework for Java and Kotlin, built on top of your favorite web server. +Jooby is a modular, high-performance web framework for Java and Kotlin. Designed for simplicity and speed, it gives you the freedom to build on your favorite server with a clean, modern API. .Welcome! [source,java,role="primary"] diff --git a/docs/asciidoc/migration/3.x.adoc b/docs/asciidoc/migration/3.x.adoc index e25187c5bd..78abe47887 100644 --- a/docs/asciidoc/migration/3.x.adoc +++ b/docs/asciidoc/migration/3.x.adoc @@ -64,9 +64,10 @@ Kotlin was removed from core, you need to the `jooby-kotlin` dependency: |=== ==== Class renames +[cols="1,1,2"] |=== |2.x|3.x|Module -|io.jooby.Route.Decorator|io.jooby.Route.Filter| jooby (core) +|io.jooby.Route.Decorator| jooby (core) |io.jooby.Route.Filter |io.jooby.Kooby|io.jooby.kt.Kooby| jooby-kotlin (new module) |io.jooby.jetty.Jetty|io.jooby.jetty.JettyServer| jooby-jetty |io.jooby.netty.Netty|io.jooby.netty.NettyServer| jooby-netty @@ -87,6 +88,7 @@ Kotlin was removed from core, you need to the `jooby-kotlin` dependency: |=== ==== Method renames +[cols="1,1,2"] |=== |2.x|3.x|Description |Router.decorator(Decorator)|Router.use(Filter)| `decorator` has been deprecated in favor of `use` diff --git a/docs/asciidoc/migration/4.x.adoc b/docs/asciidoc/migration/4.x.adoc index 301f3517d0..8529e33455 100644 --- a/docs/asciidoc/migration/4.x.adoc +++ b/docs/asciidoc/migration/4.x.adoc @@ -65,18 +65,20 @@ runApp(args, new NettyServer(new ServerOptions()), App::new); |=== ==== Classes +[cols="1,1,1,4"] |=== -|3.x|4.x|Description|Module -|io.jooby.buffer.*|-| removed | jooby (core) -||io.jooby.output.*| new output API | jooby (core) -|io.jooby.MvcFactory|-| was deprecated and now removed | jooby (core) -|io.jooby.annotation.ResultType|-| removed | jooby (core) -|io.jooby.ValueNode|io.jooby.value.Value| replaced/merged | jooby (core) -|io.jooby.ValueNodeConverter|io.jooby.value.ValueConverter| replaced/merged | jooby (core) -|io.jooby.RouteSet|io.jooby.Route.Set| moved into Route and renamed to Set | jooby (core) +|3.x|4.x|Module|Description +|io.jooby.buffer.*|-| jooby (core) | removed +||io.jooby.output.*| jooby (core) | new output API +|io.jooby.MvcFactory|-| jooby (core) | was deprecated and now removed +|io.jooby.annotation.ResultType|-| jooby (core) | removed +|io.jooby.ValueNode|io.jooby.value.Value | jooby (core) | replaced/merged +|io.jooby.ValueNodeConverter|io.jooby.value.ValueConverter| jooby (core) | replaced/merged +|io.jooby.RouteSet|io.jooby.Route.Set | jooby (core) | moved into Route and renamed to Set |=== ==== Method +[cols="1,1,2"] |=== |3.x|4.x|Description |io.jooby.Jooby.setServerOptions()|Server.setOptions()| removed in favor of `Server.setOptions()` diff --git a/docs/asciidoc/modules/avaje-inject.adoc b/docs/asciidoc/modules/avaje-inject.adoc index 10106c83ca..ef68ecb4b5 100644 --- a/docs/asciidoc/modules/avaje-inject.adoc +++ b/docs/asciidoc/modules/avaje-inject.adoc @@ -57,7 +57,7 @@ Please note that the order of annotation processors is important. For example, i public class App extends Jooby { { - install(AvajeInjectModule.of()); <1> + install(AvajeInjectModule.of()); <1> get("/", ctx -> { MyService service = require(MyService.class); <2> diff --git a/docs/asciidoc/modules/openapi-ascii.adoc b/docs/asciidoc/modules/openapi-ascii.adoc index a225bbbe1f..ce7c4c6815 100644 --- a/docs/asciidoc/modules/openapi-ascii.adoc +++ b/docs/asciidoc/modules/openapi-ascii.adoc @@ -105,43 +105,47 @@ Data generation follows a flexible pipeline architecture. You start with a sourc ===== 3. Data Sources (Lookups) These functions are your entry points to locate objects within the OpenAPI definition. -[cols="2m,3,3"] +[cols="2,4,5"] |=== -|Function |Description |Example +|Function |Example | Description |operation(method, path) -|Generic lookup for an API operation. |`{{ operation("GET", "/books") }}` +|Generic lookup for an API operation. |GET(path) -|Shorthand for `operation("GET", path)`. |`{{ GET("/books") }}` +|Shorthand for `operation("GET", path)`. |POST(path) -|Shorthand for `operation("POST", path)`. |`{{ POST("/books") }}` +|Shorthand for `operation("POST", path)`. |PUT / PATCH / DELETE -|Shorthand for respective HTTP methods. |`{{ DELETE("/books/{id}") }}` +|Shorthand for respective HTTP methods. |schema(name) -|Looks up a Schema/Model definition by name. |`{{ schema("User") }}` +|Looks up a Schema/Model definition by name. |tag(name) -|Selects a specific Tag group (containing name, description, and routes). |`{{ tag("Inventory") }}` +|Selects a specific Tag group (containing name, description, and routes). |routes() -|Returns a collection of all available routes in the API. |`{% for r in routes() %}...{% endfor %}` +|Returns a collection of all available routes in the API. |server(index) -|Selects a server definition from the OpenAPI spec by index. |`{{ server(0).url }}` +|Selects a server definition from the OpenAPI spec by index. |error(code) +|`{{ statusCode(200) }}` +`{{ statusCode([200, 400]) }}` + +`{{ statusCode( {200: "OK", 400: "Bad Syntax"} ) }}` |Generates an error response object. + **Default:** `{statusCode, reason, message}`. + **Custom:** Looks for a global `error` variable map and interpolates values. @@ -152,11 +156,6 @@ These functions are your entry points to locate objects within the OpenAPI defin 1. **Int:** Default reason. + 2. **List:** `[200, 404]` + 3. **Map:** `{200: "OK", 400: "Bad Syntax"}` (Overrides defaults). -|`{{ statusCode(200) }}` - -`{{ statusCode([200, 400]) }}` - -`{{ statusCode( {200: "OK", 400: "Bad Syntax"} ) }}` |=== diff --git a/docs/asciidoc/modules/openapi.adoc b/docs/asciidoc/modules/openapi.adoc index 8aa72cc96b..190a0fd36e 100644 --- a/docs/asciidoc/modules/openapi.adoc +++ b/docs/asciidoc/modules/openapi.adoc @@ -407,7 +407,7 @@ Whitespaces (including new lines) are ignored. To introduce a new line, you must ==== Supported OpenAPI tags -[cols="3,1,1,1,4"] +[cols="1,1,1,1,4"] |=== | Tag | Main | Controller | Method | Description @@ -600,7 +600,7 @@ Keep in mind that any section found here in the template overrides existing meta === Swagger Annotations -Optionally this plugin depends on some OpenAPI annotations. To use them, you need to add a dependency to your project: +Optionally, this plugin depends on some OpenAPI annotations. To use them, you need to add a dependency to your project: [dependency, artifactId="swagger-annotations"] . diff --git a/docs/asciidoc/mvc-api.adoc b/docs/asciidoc/mvc-api.adoc index eecb611ae2..fcda32b4fa 100644 --- a/docs/asciidoc/mvc-api.adoc +++ b/docs/asciidoc/mvc-api.adoc @@ -764,7 +764,7 @@ You can access the generated routes at runtime: ==== Annotation Processor Options -[cols="1,1,1,2"] +[cols="2,1,1,4"] |=== | Option | Type | Default | Description diff --git a/docs/asciidoc/router-options.adoc b/docs/asciidoc/router-options.adoc index 4da5592570..eb225c6169 100644 --- a/docs/asciidoc/router-options.adoc +++ b/docs/asciidoc/router-options.adoc @@ -38,7 +38,7 @@ import io.jooby.Jooby } ---- -[cols="2,1,1,4"] +[cols="1,1,1,4"] |=== | Option | Type | Default | Description diff --git a/docs/asciidoc/routing.adoc b/docs/asciidoc/routing.adoc index 5ee0f206a6..347c94437f 100644 --- a/docs/asciidoc/routing.adoc +++ b/docs/asciidoc/routing.adoc @@ -14,8 +14,7 @@ A javadoc:Route[] consists of three parts: [source, java, role="primary"] ---- { - - // <1> <2> + // <1> <2> get("/foo", ctx -> { return "foo"; // <3> }); @@ -35,9 +34,8 @@ A javadoc:Route[] consists of three parts: .Kotlin [source, kotlin, role="secondary"] ---- -{ - - // <1> <2> +{ + // <1> <2> get("/foo") { "foo" // <3> } diff --git a/docs/asciidoc/tooling.adoc b/docs/asciidoc/tooling.adoc index 24fa541a80..4f5754c7cc 100644 --- a/docs/asciidoc/tooling.adoc +++ b/docs/asciidoc/tooling.adoc @@ -1,4 +1,6 @@ == Tooling and Operations +[.lead] +Streamline your development workflow with Jooby's productivity suite. This section covers essential utilities like Hot Reload for instantaneous code changes without restarting the server, and deep integration with build systems like Maven and Gradle to manage your project's lifecycle from the first line of code to the final deployment. include::configuration.adoc[] diff --git a/docs/asciidoc/web.adoc b/docs/asciidoc/web.adoc index 9bfb60e98f..6f63aa8da1 100644 --- a/docs/asciidoc/web.adoc +++ b/docs/asciidoc/web.adoc @@ -1,4 +1,6 @@ == Web +[.lead] +Everything you need to handle HTTP traffic and build robust APIs or web applications. Explore Jooby's expressive routing paradigms, request and response handling, content negotiation, and advanced web features like WebSockets and file uploads. include::mvc-api.adoc[] diff --git a/docs/js/styles/theme.css b/docs/js/styles/theme.css new file mode 100644 index 0000000000..ea246c1226 --- /dev/null +++ b/docs/js/styles/theme.css @@ -0,0 +1,1039 @@ +/* ========================================================================== + 1. CSS Variables & Theming + ========================================================================== */ +:root { + /* Brand Colors */ + --jooby-blue: #2196f3; + --jooby-blue-hover: #1976d2; + --jooby-accent: #ffa726; + + /* Light Theme (Default) */ + --bg-main: #ffffff; + --bg-surface: #f8f9fa; + --bg-callout: #f3f4f6; + --border-color: #e5e7eb; + + --text-main: #374151; + --text-muted: #6b7280; + --heading-color: #111827; + --link-color: var(--jooby-blue); + + /* Code Blocks */ + --code-bg: #282c34; /* Update this to match Atom One Dark perfectly */ + --code-text: #ffffff; + --code-inline-bg: #f1f5f9; + --code-inline-text: #be185d; + + /* Layout */ + --sidebar-width: 300px; + --content-max-width: 900px; + --border-radius: 6px; + + /* Modern Developer Font Stack */ + --font-code: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +pre.highlightjs, .highlightjs code { + background: var(--code-bg) !important; +} + +/* Optional: Make the line under the tabs a bit softer to match the new color */ +.switch { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +html[data-theme="dark"] { + --bg-main: #0f172a; + --bg-surface: #1e293b; + --bg-callout: #1e293b; + --border-color: #334155; + + --text-main: #cbd5e1; + --text-muted: #94a3b8; + --heading-color: #f8fafc; + --link-color: #38bdf8; + + --code-inline-bg: #1e293b; + --code-inline-text: #f472b6; +} + +/* ========================================================================== + 2. Base & Typography (TIGHTENED SPACING) + ========================================================================== */ +html { scroll-padding-top: 80px; } +*, ::before, ::after { box-sizing: border-box; } + +body { + background: var(--bg-main); + color: var(--text-main); + font-family: 'Open Sans', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + -webkit-font-smoothing: antialiased; +} + +/* Tighter Headings */ +h1, h2, h3, h4, h5, h6 { + color: var(--heading-color); + font-weight: 600; + line-height: 1.3; + margin-top: 1.5em; + margin-bottom: 0.5rem; +} + +h1 { font-size: 2.5rem; margin-top: 1em; letter-spacing: -0.02em; } +h2 { font-size: 1.875rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.3em; margin-top: 2em; } +h3 { font-size: 1.5rem; } +h4 { font-size: 1.25rem; } /* 20px */ +h5 { font-size: 1.125rem; } /* 18px - Just slightly larger than body text */ + +/* Optional: Make h6 distinct since it's the same size as body text */ +h6 { + font-size: 1rem; /* 16px - Same as body */ + color: var(--text-muted); + letter-spacing: 0.05em; +} + +/* ASCIIDOCTOR FIX: Pull subheadings closer when they immediately follow a parent section */ +.sectionbody > .sect2:first-child > h3, +.sect2 > .sect3:first-child > h4, +.sect3 > .sect4:first-child > h5 { + margin-top: 0.75em; +} + +p, table, blockquote { margin-top: 0; margin-bottom: 1rem; } + +a { color: var(--link-color); text-decoration: none; transition: color 0.15s ease; } +a:hover { text-decoration: underline; color: var(--jooby-blue-hover); } +hr { border: none; border-bottom: 1px solid var(--border-color); margin: 2rem 0; } + +/* ASCIIDOCTOR FIX: Fix bloated lists caused by

tags inside

  • */ +ul, ol, dl { margin-top: 0; margin-bottom: 1rem; } +li { margin-bottom: 0.35rem; } +.ulist li p, .olist li p, .dlist li p { margin-bottom: 0; } /* Kills the double-spacing */ + +/* ========================================================================== + 3. Main Content Layout + ========================================================================== */ +#header { padding: 0; margin: 0; } + +@media screen and (min-width: 768px) { + body.toc2 { padding-left: var(--sidebar-width); } + #content, #footer { + max-width: var(--content-max-width); + margin: 0 auto; + padding: 2.5rem 3rem; + } +} + +@media screen and (max-width: 767px) { + #content, #footer { padding: 1.5rem; } +} + +#content > *:first-child, +#content > #preamble:first-child .sectionbody > *:first-child { + margin-top: 0 !important; +} + +/* ========================================================================== + 4. Table of Contents (Left Sidebar) + ========================================================================== */ +@media screen and (min-width: 768px) { + #toc.toc2 { + position: fixed; + top: 0; + left: 0; + width: var(--sidebar-width); + height: 100vh; + overflow-y: auto; + background: var(--bg-surface); + border-right: 1px solid var(--border-color); + padding: 2.5rem 1.5rem; + z-index: 1000; + } +} + +@media (max-width: 767px) { #toc.toc2 { display: none; } } + +#toctitle { + font-weight: bold; + font-size: 1.1rem; + color: var(--heading-color); + margin-top: 0; + margin-bottom: 1.2rem; + padding-left: 0.2rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +#toc ul { list-style: none; margin: 0; padding: 0; font-family: inherit; } +#toc ul ul { display: none; padding-left: 1.1rem; border-left: 1px solid var(--border-color); margin: 0.2rem 0; } +#toc li.expanded > ul { display: block !important; } + +#toc a { + display: block; + padding: 0.35rem 0; + color: var(--text-muted); + font-size: 0.92rem; + line-height: 1.4; + border-left: 3px solid transparent; + transition: all 0.15s ease-in-out; + word-break: break-word; +} + +#toc a:hover { color: var(--jooby-blue); text-decoration: none; } +#toc a.active { + color: var(--jooby-blue) !important; + font-weight: 600; + border-left-color: var(--jooby-blue); + padding-left: 0.8rem; + background-color: rgba(33, 150, 243, 0.04); +} + +#toc::-webkit-scrollbar { width: 5px; } +#toc::-webkit-scrollbar-track { background: transparent; } +#toc::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 10px; } +#toc::-webkit-scrollbar-thumb:hover { background: #cccccc; } +html[data-theme="dark"] #toc::-webkit-scrollbar-thumb { background: #475569; } +html[data-theme="dark"] #toc::-webkit-scrollbar-thumb:hover { background: #64748b; } + +/* ========================================================================== + 5. Admonitions (Callouts) + ========================================================================== */ +.admonitionblock { margin-bottom: 1.5rem; } + +/* Override Asciidoctor's rigid table layout with modern Flexbox */ +.admonitionblock > table, +.admonitionblock > table > tbody, +.admonitionblock > table > tbody > tr { + display: flex; + width: 100%; +} + +.admonitionblock > table { + background: var(--admonition-bg, var(--bg-callout)); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + border-left: 4px solid var(--admonition-color, var(--jooby-blue)); + padding: 1.25rem; +} + +/* Style the Left Column (Label & Icon) */ +.admonitionblock td.icon { + border: none; + padding: 0; + padding-right: 1.25rem; + flex-shrink: 0; + display: flex; + align-items: flex-start; +} + +/* The Text "Note", "Tip", etc. */ +.admonitionblock td.icon .title { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.8rem; + color: var(--admonition-color, var(--jooby-blue)); + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Style the Main Content */ +.admonitionblock td.content { + border: none; + padding: 0; + font-size: 0.95rem; + color: var(--text-main); + line-height: 1.6; +} + +.admonitionblock td.content p:last-child { margin-bottom: 0; } + +/* --- Color Variants & Zero-Dependency CSS Icons --- */ + +/* NOTE (Blue) */ +.admonitionblock.note { + --admonition-color: var(--jooby-blue); + --admonition-bg: rgba(33, 150, 243, 0.05); /* Subtle 5% tint */ +} +.admonitionblock.note td.icon .title::before { + content: url('data:image/svg+xml;utf8,'); + display: block; width: 16px; height: 16px; +} + +/* TIP (Green) */ +.admonitionblock.tip { + --admonition-color: #10b981; + --admonition-bg: rgba(16, 185, 129, 0.05); +} +.admonitionblock.tip td.icon .title::before { + content: url('data:image/svg+xml;utf8,'); + display: block; width: 16px; height: 16px; +} + +/* WARNING (Orange/Yellow) */ +.admonitionblock.warning { + --admonition-color: #f59e0b; + --admonition-bg: rgba(245, 158, 11, 0.05); +} +.admonitionblock.warning td.icon .title::before { + content: url('data:image/svg+xml;utf8,'); + display: block; width: 16px; height: 16px; +} + +/* IMPORTANT (Red) */ +.admonitionblock.important { + --admonition-color: #ef4444; + --admonition-bg: rgba(239, 68, 68, 0.05); +} +.admonitionblock.important td.icon .title::before { + content: url('data:image/svg+xml;utf8,'); + display: block; width: 16px; height: 16px; +} + +/* ========================================================================== + 6. Code Blocks & Inline Code + ========================================================================== */ +pre { + position: relative; + background: var(--code-bg); + color: var(--code-text); + padding: 1rem 1.25rem; + border-radius: var(--border-radius); + overflow-x: auto; + + /* Use the new font */ + font-family: var(--font-code); + font-size: 0.85rem; + font-weight: 400; + line-height: 1.6; /* Slightly increased for better code readability */ + letter-spacing: -0.01em; /* Crisp rendering */ + + margin-top: 0; + margin-bottom: 1rem; +} + +/* Also update your inline code blocks */ +:not(pre) > code { + font-family: var(--font-code); +} + +pre code, pre .hljs { + color: var(--code-text); + background: transparent !important; + padding: 0 !important; + font-size: inherit; +} + +/* ========================================================================== + 6. Code Blocks & Inline Code + ========================================================================== */ + +/* 1. Style the Title as a Mac/IDE File Tab */ +.listingblock .title { + background-color: #21252b; /* Slightly darker than the code background */ + color: #abb2bf; /* Muted terminal gray */ + font-family: var(--font-code); + font-size: 0.78rem; + padding: 0.5rem 1.25rem; + margin-bottom: 0; /* Connects it to the block below */ + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + display: block; +} + +/* 2. Remove the top rounded corners of the code block so it attaches perfectly */ +.listingblock .title + .content pre { + border-top-left-radius: 0; + border-top-right-radius: 0; + margin-top: 0; +} + +/* ========================================================================== + 7. Custom UI: Tabs (Switch) & Clipboard + ========================================================================== */ +.hidden { display: none !important; } +.primary > .title { display: none; } + +.switch { + display: flex; + background-color: var(--code-bg); + border-radius: var(--border-radius) var(--border-radius) 0 0; + overflow: hidden; + border-bottom: 1px solid #444; + margin-bottom: 0; +} + +.switch--item { + padding: 0.6rem 1.25rem; + font-size: 0.85rem; + font-weight: 600; + color: #999; + cursor: pointer; + background-color: var(--code-bg); + transition: all 0.2s ease; + user-select: none; +} + +.switch--item:hover { color: #fff; background-color: #444; } +.switch--item.selected { + color: var(--jooby-accent); + background-color: #222; + box-shadow: inset 0 -2px 0 var(--jooby-accent); +} + +.primary .content pre { + margin-top: 0 !important; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.clipboard { + position: absolute; + top: 0.5rem !important; + right: 0.5rem !important; + bottom: auto !important; + height: 32px; + width: 32px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + padding: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + opacity: 0; +} + +pre:hover .clipboard { opacity: 1; } +.clipboard:hover { background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.4); } +.clipboard:active { background: rgba(255, 255, 255, 0.3); } +.clipboard img { filter: invert(1); opacity: 0.8; height: 16px; width: 16px; } + +/* ========================================================================== + 8. Theme Toggle Button Styles + ========================================================================== */ +.theme-toggle { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--bg-surface); + color: var(--text-main); + border: 1px solid var(--border-color); + border-radius: 50%; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + z-index: 1001; + transition: all 0.2s ease; +} + +.theme-toggle:hover { + transform: scale(1.1); + border-color: var(--jooby-blue); + color: var(--jooby-blue); +} + +.theme-toggle svg { width: 22px; height: 22px; fill: currentColor; } +.theme-toggle .moon-icon { display: none; } +html[data-theme="dark"] .theme-toggle .moon-icon { display: block; } +html[data-theme="dark"] .theme-toggle .sun-icon { display: none; } + +/* ========================================================================== + 9. Header Anchor Links (Deep Linking) + ========================================================================== */ +h2, h3, h4, h5, h6 { + position: relative; +} + +h2 > a.anchor, +h3 > a.anchor, +h4 > a.anchor, +h5 > a.anchor, +h6 > a.anchor { + position: absolute; + left: -1.5rem; + top: 0; + text-decoration: none; + opacity: 0; + transition: opacity 0.2s ease; + color: var(--text-muted); + font-weight: 400; +} + +h2 > a.anchor::before, +h3 > a.anchor::before, +h4 > a.anchor::before, +h5 > a.anchor::before, +h6 > a.anchor::before { + content: "#"; /* Modern hash symbol instead of the old section mark */ +} + +/* Show the anchor on hover */ +h2:hover > a.anchor, +h3:hover > a.anchor, +h4:hover > a.anchor, +h5:hover > a.anchor, +h6:hover > a.anchor { + opacity: 1; +} + +h2 > a.anchor:hover, +h3 > a.anchor:hover, +h4 > a.anchor:hover, +h5 > a.anchor:hover, +h6 > a.anchor:hover { + color: var(--jooby-blue); +} + +/* ========================================================================== + 10. Hero Section (Preamble) + ========================================================================== */ +#preamble .sectionbody > h2.discrete { + font-size: 3.5rem; + font-weight: 800; + letter-spacing: -0.03em; + margin-top: 0; + margin-bottom: 1rem; + /* Optional: A subtle gradient using Jooby's blue to accent */ + background: linear-gradient(135deg, var(--jooby-blue) 0%, #8b5cf6 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +#preamble .sectionbody > .paragraph:first-of-type p { + font-size: 1.25rem; + color: var(--text-muted); + max-width: 800px; + line-height: 1.7; +} + +/* ========================================================================== + 12. Print Styles (For "Save to PDF") + ========================================================================== */ +@media print { + /* Force light theme for printing */ + :root { + --bg-main: #ffffff !important; + --text-main: #000000 !important; + --heading-color: #000000 !important; + } + + body { + background: white !important; + color: black !important; + font-size: 11pt; /* Better for paper */ + } + + /* Hide UI elements */ + #toc, .theme-toggle, .clipboard, .version-selector, .switch { + display: none !important; + } + + /* Reset layout constraints */ + body.toc2, #content, #header, #footer { + padding: 0 !important; + margin: 0 !important; + max-width: 100% !important; + } + + /* Prevent awkward page breaks */ + h2, h3, h4, h5 { page-break-after: avoid; } + pre, blockquote, table, img { page-break-inside: avoid; } + + /* Show link URLs explicitly on paper */ + #content a::after { + content: " (" attr(href) ")"; + font-size: 0.85em; + color: #666; + } + /* Don't print URLs for internal anchor links */ + #content a[href^="#"]::after { + content: ""; + } +} + +.badge { + display: inline-block; + padding: 0.15em 0.5em; + font-size: 0.75em; + font-weight: 700; + border-radius: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + vertical-align: middle; + margin-left: 0.5em; +} + +.badge.experimental { background: #f59e0b; color: #fff; } +.badge.deprecated { background: #ef4444; color: #fff; } +.badge.new { background: #10b981; color: #fff; } + +/* ========================================================================== + 13. Callouts (Code Pointers) + ========================================================================== */ + +/* 1. The badge INSIDE the code block (created via JavaScript) */ +.conum-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: var(--code-inline-text); /* The Pink/Red */ + color: #ffffff; + font-size: 0.85rem; + font-weight: 700; + font-style: normal; +} + +/* 2. The list BELOW the code block */ +.colist { + margin-top: 1rem; + margin-bottom: 1.5rem; +} + +.colist table { border: none !important; background: transparent !important; margin: 0 !important;} +.colist tr { background: transparent !important; } +.colist td { border: none !important; padding: 0.4rem 0.5rem !important; vertical-align: top !important; color: var(--text-main); } +.colist td:first-child { padding-left: 0 !important; width: 2.5rem !important; } + +/* The dark circle in the list below the code block */ +.colist .conum { + display: inline-flex !important; + align-items: center; + justify-content: center; + width: 1.5rem !important; + height: 1.5rem !important; + border-radius: 50% !important; + background-color: var(--text-main) !important; + color: var(--bg-main) !important; + font-size: 0.85rem !important; + font-weight: 700 !important; + font-style: normal !important; + position: relative; +} + +/* Hide fallback text Asciidoctor injects */ +.colist i.conum[data-value] { color: transparent !important; } +.colist i.conum[data-value]::after { + content: attr(data-value); + color: var(--bg-main) !important; + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%, -50%); +} +.colist .conum + b { display: none !important; } + +/* ========================================================================== + 13. Callouts (Code Pointers & Lists) + ========================================================================== */ + +/* 1. Prevent the Flash: Keep background solid, micro-fade the text only */ +pre.highlightjs code { + opacity: 0; + transition: opacity 0.05s ease-out; +} +pre.highlightjs.badges-loaded code { + opacity: 1; +} + +/* 2. The Pink Badge INSIDE the code block */ +.conum-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: var(--code-inline-text); + color: #ffffff !important; + font-size: 0.85rem; + font-weight: 700; + font-style: normal; + flex-shrink: 0; +} + +/* 3. The List BELOW the code block */ +.colist.arabic ol { + list-style: none !important; + padding-left: 0; + margin-top: 1.5rem; + counter-reset: colist-counter; +} + +.colist.arabic li { + margin-bottom: 1.25rem; + counter-increment: colist-counter; + position: relative; + /* This creates a "gutter" for the badge */ + padding-left: 2.5rem; + min-height: 1.5rem; +} + +/* Draws the Dark Badge */ +.colist.arabic li::before { + content: counter(colist-counter); + position: absolute; + left: 0; + top: 0; + display: flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: var(--text-main); + color: var(--bg-main); + font-size: 0.85rem; + font-weight: 700; +} + +/* Ensures all nested content (paragraphs, ulist, etc.) aligns to the right of the badge */ +.colist.arabic li > *:first-child { + display: block; + margin-top: 0; +} + +.colist.arabic li p { + margin-bottom: 0.5rem; + line-height: 1.5rem; +} + +/* Nested Bullet Lists */ +.colist.arabic li .ulist { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.colist.arabic li .ulist ul { + list-style: disc; + padding-left: 1.25rem; /* Standard bullet indentation */ + margin: 0; +} + +.colist.arabic li .ulist li { + padding-left: 0; /* Reset the callout padding for nested bullets */ + margin-bottom: 0.25rem; + counter-increment: none; +} + +.colist.arabic li .ulist li::before { + content: none; /* No badges on nested bullets */ +} + +/* --- Utilities --- */ + +.visually-hidden { + position: absolute !important; width: 1px !important; height: 1px !important; + padding: 0 !important; margin: -1px !important; overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; +} + +h2 > a.anchor.copied::before, h3 > a.anchor.copied::before, h4 > a.anchor.copied::before, +h5 > a.anchor.copied::before, h6 > a.anchor.copied::before { + content: "✓"; color: #10b981; +} + +/* ========================================================================== + 14. Tables + ========================================================================== */ + +table.tableblock { + width: 100%; + /* Switch back to fixed to strictly honor [cols="1,1,4"] */ + table-layout: fixed !important; + border-collapse: separate; + border-spacing: 0; + margin: 1.5rem 0; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + overflow: hidden; +} + +/* 1. Reset all columns to allow wrapping by default */ +table.tableblock td, +table.tableblock th { + padding: 0.75rem 1.25rem; + border-bottom: 1px solid var(--border-color); + font-size: 0.9rem; + line-height: 1.6; + text-align: left; + vertical-align: top; + word-wrap: break-word; + overflow-wrap: anywhere; +} + +/* 2. Header Polish */ +table.tableblock thead th { + background-color: var(--bg-surface); + font-weight: 600; + border-bottom: 2px solid var(--border-color); + color: var(--heading-color); + white-space: nowrap; /* Keep headers clean on one line */ +} + +/* 3. Smart First Column Handling + If you have NOT defined specific columns, we force it to fit. + If you HAVE defined [cols], we let it wrap to fit your proportions. */ +table.tableblock:not(:has(colgroup col[style*="width"])) td:first-child { + white-space: nowrap; + width: 1%; /* Shrink-wrap effect */ +} + +/* 4. Column Spacing & Visuals */ +table.tableblock td:first-child { + font-weight: 500; + color: var(--heading-color); +} + +/* Ensure code blocks inside tables don't have extra margins */ +table.tableblock td p { + margin: 0; +} +table.tableblock td p + p { + margin-top: 0.5rem; +} + +table.tableblock tbody tr:hover { background-color: var(--bg-callout); } +table.tableblock tbody tr:last-child td { border-bottom: none; } + +/* ========================================================================== + 15. Keyboard Shortcuts (kbd) + ========================================================================== */ +kbd { + display: inline-block; + padding: 0.1rem 0.4rem; + font-family: var(--font-code); /* Uses JetBrains Mono for that tech feel */ + font-size: 0.8rem; + font-weight: 500; + color: var(--text-main); + background-color: var(--bg-surface); + /* The "3D" Key Effect */ + border: 1px solid var(--border-color); + border-radius: 4px; + box-shadow: + 0 1px 0 rgba(0, 0, 0, 0.2), + inset 0 0 0 2px var(--bg-main); + margin: 0 0.15rem; + vertical-align: middle; + line-height: 1.2; +} + +/* ========================================================================== + 16. Responsive / Mobile Adjustments + ========================================================================== */ + +@media screen and (max-width: 768px) { + /* 1. Create a top bar so the button isn't just floating over text */ + #header { + padding-top: 4rem !important; /* Push content down to clear the bar */ + } + + .mobile-nav-bar { + display: flex; + align-items: center; + justify-content: space-between; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 3.5rem; + background-color: var(--bg-main); + border-bottom: 1px solid var(--border-color); + padding: 0 1.5rem; + z-index: 10001; /* Higher than the sidebar */ + } + + /* 2. Reposition the button inside the new bar */ + #menu-toggle { + position: static !important; /* Remove the fixed positioning from before */ + margin-left: auto; + } +} + +@media screen and (max-width: 768px) { + /* 1. Make tables scrollable horizontally without breaking the page */ + .tableblock { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ + } + + /* 2. Prevent the "First Column" from taking up too much room on mobile */ + table.tableblock td:first-child { + white-space: normal !important; /* Allow wrapping on small screens */ + min-width: 120px; + } + + /* 3. Slightly reduce padding to save precious horizontal space */ + table.tableblock th, + table.tableblock td { + padding: 0.5rem 0.75rem !important; + font-size: 0.85rem; + } +} + +/* Hamburger Button Styling */ +.hamburger { + display: none; /* Hidden by default on Desktop */ + flex-direction: column; + justify-content: space-around; + width: 2rem; + height: 2rem; + background: transparent; + border: none; + cursor: pointer; + padding: 0; + z-index: 1001; +} + +.hamburger span { + width: 2rem; + height: 0.25rem; + background: var(--text-main); + border-radius: 10px; + transition: all 0.3s linear; +} + +/* Animate Hamburger to X */ +.hamburger.open span:nth-child(1) { + transform: rotate(45deg) translate(5px, 5px); +} +.hamburger.open span:nth-child(2) { + opacity: 0; +} +.hamburger.open span:nth-child(3) { + transform: rotate(-45deg) translate(7px, -6px); +} + +@media screen and (max-width: 768px) { + /* 1. Force the navigation to be a fixed mobile drawer */ + #toc, .nav-container, #sidebar { + position: fixed !important; + top: 0 !important; + left: -100% !important; /* Start off-screen */ + width: 80% !important; /* Take up most of the screen */ + max-width: 300px !important; + height: 100vh !important; + background-color: var(--bg-main) !important; /* Ensure it's not transparent */ + z-index: 9999 !important; /* Sit on top of everything */ + transition: left 0.3s ease-in-out !important; + display: block !important; /* Ensure it's not hidden with display:none */ + box-shadow: 5px 0 15px rgba(0,0,0,0.5); + padding: 2rem 1rem !important; + overflow-y: auto !important; + } + + /* 2. Slide in when the 'active' class is applied */ + #toc.active, .nav-container.active, #sidebar.active { + left: 0 !important; + } + + /* 3. Ensure the hamburger button stays visible and on top */ + #menu-toggle { + display: flex !important; + position: fixed !important; + top: 1rem; + right: 1rem; + z-index: 10000 !important; + background: var(--bg-surface); + padding: 5px; + border-radius: var(--border-radius); + } +} + +@media screen and (max-width: 768px) { + .hamburger { + display: flex; /* Show on Mobile */ + position: fixed; + top: 1rem; + right: 1rem; + } + + /* Target your specific sidebar class (usually .nav-container or #toc) */ + #sidebar, .nav-container { + position: fixed; + top: 0; + left: -100%; /* Hide off-screen */ + width: 280px; + height: 100vh; + background: var(--bg-main); + transition: left 0.3s ease-in-out; + z-index: 1000; + box-shadow: 2px 0 10px rgba(0,0,0,0.3); + } + + /* When the 'active' class is added via JS, slide it in */ + #sidebar.active, .nav-container.active { + left: 0; + } +} + +/* ========================================================================== + 17. Print Styles (Ink-Friendly PDF) + ========================================================================== */ + +@media print { + /* 1. Force a clean white background and black text for legibility */ + body { + background: white !important; + color: black !important; + font-size: 11pt; + } + + /* 2. Remove the sidebar, header, and footer to focus on the content */ + #header, #footer, #sidebar, .nav-container, .edit-link { + display: none !important; + } + + #content { + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + /* 3. Convert Code Blocks to a light theme for the printer */ + pre { + background: #f5f5f5 !important; + color: #333 !important; + border: 1px solid #ddd !important; + page-break-inside: avoid; /* Prevents code from splitting across pages */ + } + + pre code { + color: #333 !important; + } + + /* 4. Ensure Tables expand and borders are visible */ + table.tableblock { + border: 1px solid #999 !important; + page-break-inside: auto; + } + + tr { + page-break-inside: avoid; + page-break-after: auto; + } + + /* 5. Show the actual URL next to links (useful for paper) */ + a[href^="http"]:after { + content: " (" attr(href) ")"; + font-size: 90%; + color: #666; + } +} + diff --git a/docs/js/styles/toc.css b/docs/js/styles/toc.css deleted file mode 100644 index e040e7b046..0000000000 --- a/docs/js/styles/toc.css +++ /dev/null @@ -1,104 +0,0 @@ -html { - /* This tells the browser: "When scrolling to an anchor link (#id), - always leave 80px of space at the top so it isn't hidden by the top nav." */ - scroll-padding-top: 80px; -} - -/* 1. Root Container & Sticky Positioning */ -#toc { - position: -webkit-sticky; - position: sticky; - top: 2rem; - max-height: calc(100vh - 4rem); - overflow-y: auto; - padding-right: 1.5rem; - font-family: inherit; -} - -/* 2. List Structure Reset */ -#toc ul { - list-style: none; - margin: 0; - padding: 0; -} - -/* 3. INITIAL COLLAPSED STATE -Hides all nested levels (sectlevel2, 3, etc.) by default. -*/ -#toc ul ul { - display: none; - padding-left: 1.1rem; - border-left: 1px solid #eeeeee; - margin: 0.2rem 0; -} - -/* 4. EXPANSION LOGIC - Only shows the direct child
      when the parent
    • is marked .expanded - */ -#toc li.expanded > ul { - display: block !important; -} - -/* 5. Link Aesthetics */ -#toc a { - display: block; - padding: 0.3rem 0; - color: #555555; - text-decoration: none; - font-size: 0.92rem; - line-height: 1.4; - border-left: 3px solid transparent; /* Base for the active indicator */ - transition: all 0.15s ease-in-out; - word-break: break-word; -} - -/* 6. HOVER & ACTIVE STATES (Jooby Blue) */ -#toc a:hover { - color: #2196f3; -} - -#toc a.active { - color: #2196f3 !important; - font-weight: 600; - border-left-color: #2196f3; - padding-left: 0.8rem; - background-color: rgba(33, 150, 243, 0.04); /* Subtle highlight behind active link */ -} - -/* 7. Custom Scrollbar for the Sidebar (Modern Browsers) */ -#toc::-webkit-scrollbar { - width: 5px; -} - -#toc::-webkit-scrollbar-track { - background: transparent; -} - -#toc::-webkit-scrollbar-thumb { - background: #e0e0e0; - border-radius: 10px; -} - -#toc::-webkit-scrollbar-thumb:hover { - background: #cccccc; -} - -/* 8. Mobile Hide (Optional) -Hides the sidebar on screens smaller than 768px -*/ -@media (max-width: 767px) { - #toc { - display: none; - } -} - -/* ========================================================================== - NEW: Title Styling -========================================================================== */ -#toctitle { - font-weight: bold; - font-size: 1.1rem; - color: #333333; - margin-bottom: 0.8rem; - padding-left: 0.2rem; -} diff --git a/docs/src/main/java/io/jooby/adoc/DocGenerator.java b/docs/src/main/java/io/jooby/adoc/DocGenerator.java index fbb8beb01d..ec71e58263 100644 --- a/docs/src/main/java/io/jooby/adoc/DocGenerator.java +++ b/docs/src/main/java/io/jooby/adoc/DocGenerator.java @@ -245,22 +245,27 @@ private static Options createOptions(Path basedir, Path outdir, String version, throws IOException { var attributes = Attributes.builder(); + attributes.linkCss(true); + attributes.experimental(true); + attributes.styleSheetName("theme.css"); + attributes.stylesDir("js/styles"); + attributes.setAnchors(true); + attributes.attribute("docinfo", "shared"); + attributes.attribute("docfile", docfile.toString()); attributes.attribute("uiVersion", uiVersion); attributes.attribute("love", "♡"); attributes.attribute("docinfo", "shared"); attributes.title(title == null ? "jooby: do more! more easily!!" : "jooby: " + title); attributes.tableOfContents(Placement.LEFT); - attributes.attribute("toclevels", "4"); + attributes.attribute("toclevels", "5"); attributes.setAnchors(true); attributes.attribute("sectlinks", ""); attributes.sectionNumbers(true); -// attributes.attribute("sectnumlevels", "5"); attributes.linkAttrs(true); attributes.noFooter(true); attributes.attribute("idprefix", ""); attributes.attribute("idseparator", "-"); - attributes.icons("font"); attributes.attribute("description", "The modular micro web framework for Java"); attributes.attribute( "keywords", "Java, Modern, Micro, Web, Framework, Reactive, Lightweight, Microservices"); @@ -268,7 +273,7 @@ private static Options createOptions(Path basedir, Path outdir, String version, attributes.sourceHighlighter("highlightjs"); attributes.attribute("highlightjsdir", "js"); // agate, atom-one-dark, tomorrow-night-bright, tokyo-night-dark - attributes.attribute("highlightjs-theme", "agate"); + attributes.attribute("highlightjs-theme", "atom-one-dark"); attributes.attribute("favicon", "images/favicon96.png"); // versions: diff --git a/docs/src/main/java/io/jooby/adoc/DocPostprocessor.java b/docs/src/main/java/io/jooby/adoc/DocPostprocessor.java index ee2a124f0b..5a1edf228a 100644 --- a/docs/src/main/java/io/jooby/adoc/DocPostprocessor.java +++ b/docs/src/main/java/io/jooby/adoc/DocPostprocessor.java @@ -86,6 +86,7 @@ private static void headerIds(Document doc) { headerIds(doc, 3); headerIds(doc, 4); headerIds(doc, 5); + headerIds(doc, 6); } private static void toc(Document doc) { From 94782057bf6401aba0edd4a036ac2cf1bc90dd09 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 26 Feb 2026 15:43:58 -0300 Subject: [PATCH 21/31] - implement `@Project` annotation on MVC ref #3853 --- jooby/src/main/java/io/jooby/Projected.java | 13 ++++- .../test/java/io/jooby/ProjectionTest.java | 18 ++++++ .../java/io/jooby/internal/apt/MvcRoute.java | 37 ++++++++++-- .../io/jooby/internal/apt/TypeDefinition.java | 10 ++++ .../java/io/jooby/internal/apt/Types.java | 2 +- .../src/test/java/tests/i3853/C3853.java | 42 ++++++++++++++ .../src/test/java/tests/i3853/Issue3853.java | 45 ++++++++++++++ .../{i3854/U3854.java => i3853/U3853.java} | 4 +- .../src/test/java/tests/i3854/C3854.java | 20 ------- .../src/test/java/tests/i3854/Issue3854.java | 21 ------- .../jooby/avaje/jsonb/AvajeJsonbModule.java | 25 +++++++- .../test/java/io/jooby/i3853/Issue3853.java | 58 ++++++++++++++++++- 12 files changed, 239 insertions(+), 56 deletions(-) create mode 100644 modules/jooby-apt/src/test/java/tests/i3853/C3853.java create mode 100644 modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java rename modules/jooby-apt/src/test/java/tests/{i3854/U3854.java => i3853/U3853.java} (62%) delete mode 100644 modules/jooby-apt/src/test/java/tests/i3854/C3854.java delete mode 100644 modules/jooby-apt/src/test/java/tests/i3854/Issue3854.java diff --git a/jooby/src/main/java/io/jooby/Projected.java b/jooby/src/main/java/io/jooby/Projected.java index df14577fa9..a230bdcd74 100644 --- a/jooby/src/main/java/io/jooby/Projected.java +++ b/jooby/src/main/java/io/jooby/Projected.java @@ -5,6 +5,7 @@ */ package io.jooby; +import java.util.*; import java.util.function.Consumer; /** @@ -25,7 +26,17 @@ private Projected(T value, Projection projection) { @SuppressWarnings("unchecked") public static Projected wrap(T value) { - return new Projected<>(value, Projection.of((Class) value.getClass())); + return new Projected(value, Projection.of(computeProjectionType(value))); + } + + @SuppressWarnings("rawtypes") + private static Class computeProjectionType(Object value) { + return switch (value) { + case Set col -> col.isEmpty() ? Object.class : col.iterator().next().getClass(); + case Collection col -> col.isEmpty() ? Object.class : col.iterator().next().getClass(); + case Optional optional -> optional.isEmpty() ? Object.class : optional.get().getClass(); + default -> value.getClass(); + }; } public static Projected wrap(T value, Projection projection) { diff --git a/jooby/src/test/java/io/jooby/ProjectionTest.java b/jooby/src/test/java/io/jooby/ProjectionTest.java index a5f816dcd6..2bc16d310b 100644 --- a/jooby/src/test/java/io/jooby/ProjectionTest.java +++ b/jooby/src/test/java/io/jooby/ProjectionTest.java @@ -316,4 +316,22 @@ public void testValidateToggle() { assertTrue(p.getChildren().containsKey("extraPolymorphicField")); assertTrue(p.getChildren().get("address").getChildren().containsKey("zipcode")); } + + @Test + public void testTopLevelListProjection() { + // If your route returns a List, the root projection type is just User.class. + // The JSON engines (Jackson/Avaje) will naturally apply this User projection + // to every element in the JSON array. + Projection projection = Projection.of(User.class).include("id, email"); + + // Assert: Avaje view string + assertEquals("(id,email)", projection.toView()); + + // Assert: Tree Structure validates against User + assertEquals(User.class, projection.getType()); + assertEquals(2, projection.getChildren().size()); + assertTrue(projection.getChildren().containsKey("id")); + assertTrue(projection.getChildren().containsKey("email")); + assertFalse(projection.getChildren().containsKey("name")); + } } diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java index 939794e3f1..7b53401600 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java @@ -65,13 +65,23 @@ public MvcContext getContext() { return context; } + public String getProjection() { + var project = AnnotationSupport.findAnnotationByName(method, Types.PROJECT); + if (project != null) { + return AnnotationSupport.findAnnotationValue(project, VALUE).stream().findFirst().orElse(""); + } + return null; + } + public TypeDefinition getReturnType() { var processingEnv = context.getProcessingEnvironment(); var types = processingEnv.getTypeUtils(); var elements = processingEnv.getElementUtils(); - var project = AnnotationSupport.findAnnotationByName(method, Types.PROJECT); - if (project != null) { - return new TypeDefinition(types, elements.getTypeElement(Types.PROJECTED).asType()); + var isProjection = + !returnType.is(Types.PROJECTED) + && AnnotationSupport.findAnnotationByName(method, Types.PROJECT) != null; + if (isProjection) { + return new TypeDefinition(types, elements.getTypeElement(Types.PROJECTED).asType(), true); } else if (returnType.isVoid()) { return new TypeDefinition(types, elements.getTypeElement("io.jooby.StatusCode").asType()); } else if (isSuspendFun()) { @@ -223,6 +233,12 @@ public List generateHandlerCall(boolean kt) { var returnTypeGenerics = getReturnType().getArgumentsString(kt, false, Set.of(TypeKind.TYPEVAR)); var returnTypeString = type(kt, getReturnType().toString()); + var customReturnType = getReturnType(); + if (customReturnType.isProjection()) { + // Override for projection + returnTypeGenerics = ""; + returnTypeString = Types.PROJECTED + "<" + returnType + ">"; + } boolean nullable = false; if (kt) { @@ -321,7 +337,10 @@ public List generateHandlerCall(boolean kt) { buffer.add(statement(indent(2), "return statusCode", semicolon(kt))); } else { controllerVar(kt, buffer); - var cast = getReturnType().getArgumentsString(kt, false, Set.of(TypeKind.TYPEVAR)); + var cast = + customReturnType.isProjection() + ? "" + : customReturnType.getArgumentsString(kt, false, Set.of(TypeKind.TYPEVAR)); var kotlinNotEnoughTypeInformation = !cast.isEmpty() && kt ? "" : ""; var call = of( @@ -333,7 +352,15 @@ public List generateHandlerCall(boolean kt) { setUncheckedCast(true); call = kt ? call + " as " + returnTypeString : "(" + returnTypeString + ") " + call; } - buffer.add(statement(indent(2), "return ", call, kt && nullable ? "!!" : "", semicolon(kt))); + if (customReturnType.isProjection()) { + var projected = + of(Types.PROJECTED, ".wrap(", call, ").include(", string(getProjection()), ")"); + buffer.add( + statement(indent(2), "return ", projected, kt && nullable ? "!!" : "", semicolon(kt))); + } else { + buffer.add( + statement(indent(2), "return ", call, kt && nullable ? "!!" : "", semicolon(kt))); + } } buffer.add(statement("}", System.lineSeparator())); if (uncheckedCast) { diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java index 7213a0df5a..089adc4bc2 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/TypeDefinition.java @@ -22,12 +22,22 @@ public class TypeDefinition { private final TypeMirror type; private final TypeMirror unwrapType; private final TypeMirror rawType; + private final boolean projection; public TypeDefinition(Types types, TypeMirror type) { + this(types, type, false); + } + + public TypeDefinition(Types types, TypeMirror type, boolean projection) { this.typeUtils = types; this.type = type; this.unwrapType = unwrapType(type); this.rawType = typeUtils.erasure(unwrapType); + this.projection = projection; + } + + public boolean isProjection() { + return projection; } public String toSourceCode(boolean kt) { diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/Types.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/Types.java index cf8b603976..a0b41a10dd 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/Types.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/Types.java @@ -15,7 +15,7 @@ class Types { static final String PROJECT = "io.jooby.annotation.Project"; - static final String PROJECTED = "io.jooby.annotation.Projected"; + static final String PROJECTED = "io.jooby.Projected"; static final Set BUILT_IN = Set.of( String.class.getName(), diff --git a/modules/jooby-apt/src/test/java/tests/i3853/C3853.java b/modules/jooby-apt/src/test/java/tests/i3853/C3853.java new file mode 100644 index 0000000000..91ff160d94 --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/i3853/C3853.java @@ -0,0 +1,42 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package tests.i3853; + +import java.util.List; +import java.util.Optional; + +import io.jooby.Projected; +import io.jooby.annotation.GET; +import io.jooby.annotation.Path; +import io.jooby.annotation.Project; + +@Path("/3854") +public class C3853 { + @GET("/stub") + @Project("(id, name)") + public U3853 projectUser() { + return new U3853(1, "Projected User", "Projected", "User"); + } + + @GET("/optinal") + @Project("(id, name)") + public Optional findUser() { + return Optional.of(new U3853(1, "Projected User", "Projected", "User")); + } + + @GET("/list") + @Project("(id, name)") + public List findUsers() { + return List.of(new U3853(1, "Projected User", "Projected", "User")); + } + + @GET("/list") + @Project("(id, name)") + public Projected projected() { + return Projected.wrap(new U3853(1, "Projected User", "Projected", "User")) + .include("(id, name)"); + } +} diff --git a/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java b/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java new file mode 100644 index 0000000000..9ed33d7293 --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package tests.i3853; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.jooby.apt.ProcessorRunner; + +public class Issue3853 { + @Test + public void shouldSupportNameAttribute() throws Exception { + new ProcessorRunner(new C3853()) + .withSourceCode( + source -> { + Assertions.assertTrue( + source.contains( + "return io.jooby.Projected.wrap(c.projectUser()).include(\"(id, name)\");")); + Assertions.assertTrue( + source.contains( + "return io.jooby.Projected.wrap(c.findUser()).include(\"(id, name)\");")); + Assertions.assertTrue( + source.contains( + "return io.jooby.Projected.wrap(c.findUsers()).include(\"(id, name)\");")); + Assertions.assertTrue(source.contains("return c.projected();")); + }) + .withSourceCode( + true, + source -> { + Assertions.assertTrue( + source.contains( + "return io.jooby.Projected.wrap(c.projectUser()).include(\"(id, name)\")")); + Assertions.assertTrue( + source.contains( + "return io.jooby.Projected.wrap(c.findUser()).include(\"(id, name)\")")); + Assertions.assertTrue( + source.contains( + "return io.jooby.Projected.wrap(c.findUsers()).include(\"(id, name)\")")); + Assertions.assertTrue(source.contains("return c.projected()")); + }); + } +} diff --git a/modules/jooby-apt/src/test/java/tests/i3854/U3854.java b/modules/jooby-apt/src/test/java/tests/i3853/U3853.java similarity index 62% rename from modules/jooby-apt/src/test/java/tests/i3854/U3854.java rename to modules/jooby-apt/src/test/java/tests/i3853/U3853.java index 6d0bcdc701..d143fc39b4 100644 --- a/modules/jooby-apt/src/test/java/tests/i3854/U3854.java +++ b/modules/jooby-apt/src/test/java/tests/i3853/U3853.java @@ -3,6 +3,6 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package tests.i3854; +package tests.i3853; -public record U3854(long id, String name, String firstName, String lastName) {} +public record U3853(long id, String name, String firstName, String lastName) {} diff --git a/modules/jooby-apt/src/test/java/tests/i3854/C3854.java b/modules/jooby-apt/src/test/java/tests/i3854/C3854.java deleted file mode 100644 index c657cc3c96..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3854/C3854.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3854; - -import io.jooby.annotation.GET; -import io.jooby.annotation.Path; -import io.jooby.annotation.Project; -import tests.i3804.Base3804; - -@Path("/3854") -public class C3854 extends Base3804 { - @GET() - @Project("(id, name)") - public U3854 projectUser() { - return new U3854(1, "Projected User", "Projected", "User"); - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3854/Issue3854.java b/modules/jooby-apt/src/test/java/tests/i3854/Issue3854.java deleted file mode 100644 index 01b154edae..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3854/Issue3854.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3854; - -import org.junit.jupiter.api.Test; - -import io.jooby.apt.ProcessorRunner; - -public class Issue3854 { - @Test - public void shouldSupportNameAttribute() throws Exception { - new ProcessorRunner(new C3854()) - .withSourceCode( - source -> { - System.out.println(source); - }); - } -} diff --git a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java index 0a1ab457a3..9fc7ebad4a 100644 --- a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java +++ b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.lang.reflect.Type; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -128,8 +129,26 @@ private void encodeProjection(JsonWriter writer, Projected projected) { var view = (JsonView) viewCache.computeIfAbsent( - type.getName() + viewString, k -> jsonb.type(type).view(viewString)); - - view.toJson(value, writer); + value.getClass().getName() + viewString, + k -> { + var jsonbType = jsonb.type(type); + jsonbType = + switch (value) { + case Set ignored -> jsonbType.set(); + case Collection ignored -> jsonbType.list(); + default -> jsonbType; + }; + return jsonbType.view(viewString); + }); + if (value instanceof Optional optional) { + if (optional.isEmpty()) { + writer.serializeNulls(true); + writer.nullValue(); + } else { + view.toJson(optional.get(), writer); + } + } else { + view.toJson(value, writer); + } } } diff --git a/tests/src/test/java/io/jooby/i3853/Issue3853.java b/tests/src/test/java/io/jooby/i3853/Issue3853.java index 50fe90abb6..89675bfc26 100644 --- a/tests/src/test/java/io/jooby/i3853/Issue3853.java +++ b/tests/src/test/java/io/jooby/i3853/Issue3853.java @@ -7,9 +7,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import io.avaje.jsonb.Json; import io.jooby.Extension; @@ -51,6 +49,28 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { ctx -> { return Projected.wrap(createUser(), STUB); }); + + app.get( + "/stub-list", + ctx -> { + return Projected.wrap(List.of(createUser())).include("(id, name)"); + }); + + app.get( + "/stub-set", + ctx -> { + return Projected.wrap(Set.of(createUser())).include("(id, name)"); + }); + app.get( + "/stub-optional", + ctx -> { + return Projected.wrap(Optional.of(createUser())).include("(id, name)"); + }); + app.get( + "/stub-optional-null", + ctx -> { + return Projected.wrap(Optional.empty()).include("(id, name)"); + }); app.get( "/stub/meta", ctx -> { @@ -104,6 +124,38 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { {"id":"cobb-001","name":"Dom Cobb"} """); }); + http.get( + "/stub-list", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + [{"id":"cobb-001","name":"Dom Cobb"}] + """); + }); + http.get( + "/stub-set", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + [{"id":"cobb-001","name":"Dom Cobb"}] + """); + }); + http.get( + "/stub-optional", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + {"id":"cobb-001","name":"Dom Cobb"} + """); + }); + http.get( + "/stub-optional-null", + rsp -> { + assertThat(rsp.body().string()).isEqualTo("null"); + }); http.get( "/stub/address", rsp -> { From bf21582ffe3c482326ed528927e246ed5d2c911e Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 26 Feb 2026 18:52:19 -0300 Subject: [PATCH 22/31] - implement `projection` shortcut for MVC - add new test --- docs/asciidoc/context.adoc | 4 +- .../main/java/io/jooby/annotation/DELETE.java | 10 ++ .../main/java/io/jooby/annotation/GET.java | 10 ++ .../main/java/io/jooby/annotation/PATCH.java | 10 ++ .../main/java/io/jooby/annotation/POST.java | 10 ++ .../main/java/io/jooby/annotation/PUT.java | 10 ++ .../java/io/jooby/annotation/Project.java | 15 ++- modules/jooby-apt/pom.xml | 6 + .../java/io/jooby/internal/apt/MvcRoute.java | 28 ++++- .../src/test/java/tests/i3853/C3853.java | 9 +- .../src/test/java/tests/i3853/Issue3853.java | 10 +- .../jooby/avaje/jsonb/AvajeJsonbModule.java | 44 +++---- tests/src/test/java/io/jooby/i3853/A3853.java | 27 +++++ tests/src/test/java/io/jooby/i3853/C3853.java | 45 +++++++ .../test/java/io/jooby/i3853/Issue3853.java | 111 +++--------------- .../java/io/jooby/i3853/Issue3853Mvc.java | 91 ++++++++++++++ tests/src/test/java/io/jooby/i3853/L3853.java | 11 ++ tests/src/test/java/io/jooby/i3853/R3853.java | 11 ++ tests/src/test/java/io/jooby/i3853/U3853.java | 74 ++++++++++++ 19 files changed, 402 insertions(+), 134 deletions(-) create mode 100644 tests/src/test/java/io/jooby/i3853/A3853.java create mode 100644 tests/src/test/java/io/jooby/i3853/C3853.java create mode 100644 tests/src/test/java/io/jooby/i3853/Issue3853Mvc.java create mode 100644 tests/src/test/java/io/jooby/i3853/L3853.java create mode 100644 tests/src/test/java/io/jooby/i3853/R3853.java create mode 100644 tests/src/test/java/io/jooby/i3853/U3853.java diff --git a/docs/asciidoc/context.adoc b/docs/asciidoc/context.adoc index 47f5921248..abf769983c 100644 --- a/docs/asciidoc/context.adoc +++ b/docs/asciidoc/context.adoc @@ -406,7 +406,7 @@ curl -d "id=root&pass=pwd" -X POST http://localhost:8080/user <2> Form as `multi-value map` => `{id=root, pass=[pwd]}` <3> Form variable `id` => `root` <4> Form variable `pass` => `pwd` -<5> Form as `User` object => `User(id=root, pass=pwd)` +<5> Form as `U3853` object => `User(id=root, pass=pwd)` ==== Multipart @@ -482,7 +482,7 @@ curl -F id=root -F pass=root -F pic=@/path/to/local/file/profile.png http://loca <3> Form variable `id` => `root` <4> Form variable `pass` => `pwd` <5> javadoc:FileUpload[] variable `pic` -<6> Form as `User` object => `User(id=root, pass=pwd, pic=profile.png)` +<6> Form as `U3853` object => `User(id=root, pass=pwd, pic=profile.png)` [NOTE] .File Upload diff --git a/jooby/src/main/java/io/jooby/annotation/DELETE.java b/jooby/src/main/java/io/jooby/annotation/DELETE.java index 4eefcf3fd0..720324407a 100644 --- a/jooby/src/main/java/io/jooby/annotation/DELETE.java +++ b/jooby/src/main/java/io/jooby/annotation/DELETE.java @@ -57,4 +57,14 @@ * @return Consume types. */ String[] consumes() default {}; + + /** + * Field paths to include. Supports dot-notation and avaje-notation. It is a shortcut for {@link + * Project}. + * + *

      Example: id, name, address(city, zip) + * + * @return Field paths to include. Supports dot-notation and avaje-notation. + */ + String projection() default ""; } diff --git a/jooby/src/main/java/io/jooby/annotation/GET.java b/jooby/src/main/java/io/jooby/annotation/GET.java index 9d3a93e0c9..a32b1f44e0 100644 --- a/jooby/src/main/java/io/jooby/annotation/GET.java +++ b/jooby/src/main/java/io/jooby/annotation/GET.java @@ -57,4 +57,14 @@ * @return Consume types. */ String[] consumes() default {}; + + /** + * Field paths to include. Supports dot-notation and avaje-notation. It is a shortcut for {@link + * Project}. + * + *

      Example: id, name, address(city, zip) + * + * @return Field paths to include. Supports dot-notation and avaje-notation. + */ + String projection() default ""; } diff --git a/jooby/src/main/java/io/jooby/annotation/PATCH.java b/jooby/src/main/java/io/jooby/annotation/PATCH.java index 96e8ffe158..5ce1fd7311 100644 --- a/jooby/src/main/java/io/jooby/annotation/PATCH.java +++ b/jooby/src/main/java/io/jooby/annotation/PATCH.java @@ -57,4 +57,14 @@ * @return Consume types. */ String[] consumes() default {}; + + /** + * Field paths to include. Supports dot-notation and avaje-notation. It is a shortcut for {@link + * Project}. + * + *

      Example: id, name, address(city, zip) + * + * @return Field paths to include. Supports dot-notation and avaje-notation. + */ + String projection() default ""; } diff --git a/jooby/src/main/java/io/jooby/annotation/POST.java b/jooby/src/main/java/io/jooby/annotation/POST.java index b7a3e6e0d4..3e1fb597f3 100644 --- a/jooby/src/main/java/io/jooby/annotation/POST.java +++ b/jooby/src/main/java/io/jooby/annotation/POST.java @@ -57,4 +57,14 @@ * @return Consume types. */ String[] consumes() default {}; + + /** + * Field paths to include. Supports dot-notation and avaje-notation. It is a shortcut for {@link + * Project}. + * + *

      Example: id, name, address(city, zip) + * + * @return Field paths to include. Supports dot-notation and avaje-notation. + */ + String projection() default ""; } diff --git a/jooby/src/main/java/io/jooby/annotation/PUT.java b/jooby/src/main/java/io/jooby/annotation/PUT.java index e4d48d747c..61e455392d 100644 --- a/jooby/src/main/java/io/jooby/annotation/PUT.java +++ b/jooby/src/main/java/io/jooby/annotation/PUT.java @@ -57,4 +57,14 @@ * @return Consume types. */ String[] consumes() default {}; + + /** + * Field paths to include. Supports dot-notation and avaje-notation. It is a shortcut for {@link + * Project}. + * + *

      Example: id, name, address(city, zip) + * + * @return Field paths to include. Supports dot-notation and avaje-notation. + */ + String projection() default ""; } diff --git a/jooby/src/main/java/io/jooby/annotation/Project.java b/jooby/src/main/java/io/jooby/annotation/Project.java index 84791444e1..730c9d3dd0 100644 --- a/jooby/src/main/java/io/jooby/annotation/Project.java +++ b/jooby/src/main/java/io/jooby/annotation/Project.java @@ -32,6 +32,15 @@ * } * } * + * Or + * + *

      {@code
      + * @GET(projection = "id, name, address(city, zip)")
      + * public User getUser() {
      + * return userService.find(1);
      + * }
      + * }
      + * * @author edgar * @since 4.0.0 */ @@ -41,9 +50,9 @@ @Documented public @interface Project { /** - * Field paths to include. Supports dot-notation and avaje-notation. + * Example: {@code "id, name, address(city, zip)"} * - * @return The array of field paths. + * @return Avaje notation. */ - String[] value() default {}; + String value() default ""; } diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index cb94b192dd..454298979c 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -113,6 +113,12 @@ swagger-annotations test + + + org.assertj + assertj-core + test + diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java index 7b53401600..b50423de28 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java @@ -68,19 +68,35 @@ public MvcContext getContext() { public String getProjection() { var project = AnnotationSupport.findAnnotationByName(method, Types.PROJECT); if (project != null) { - return AnnotationSupport.findAnnotationValue(project, VALUE).stream().findFirst().orElse(""); + return AnnotationSupport.findAnnotationValue(project, VALUE).stream() + .findFirst() + .orElse(null); } - return null; + // look inside the method annotation + var httpMethod = annotationMap.values().iterator().next(); + var projection = AnnotationSupport.findAnnotationValue(httpMethod, "projection"::equals); + return projection.stream().findFirst().orElse(null); + } + + public boolean isProjection() { + if (returnType.is(Types.PROJECTED)) { + return false; + } + var isProjection = AnnotationSupport.findAnnotationByName(method, Types.PROJECT) != null; + if (isProjection) { + return true; + } + // look inside the method annotation + var httpMethod = annotationMap.values().iterator().next(); + var projection = AnnotationSupport.findAnnotationValue(httpMethod, "projection"::equals); + return !projection.isEmpty(); } public TypeDefinition getReturnType() { var processingEnv = context.getProcessingEnvironment(); var types = processingEnv.getTypeUtils(); var elements = processingEnv.getElementUtils(); - var isProjection = - !returnType.is(Types.PROJECTED) - && AnnotationSupport.findAnnotationByName(method, Types.PROJECT) != null; - if (isProjection) { + if (isProjection()) { return new TypeDefinition(types, elements.getTypeElement(Types.PROJECTED).asType(), true); } else if (returnType.isVoid()) { return new TypeDefinition(types, elements.getTypeElement("io.jooby.StatusCode").asType()); diff --git a/modules/jooby-apt/src/test/java/tests/i3853/C3853.java b/modules/jooby-apt/src/test/java/tests/i3853/C3853.java index 91ff160d94..51ecf302c8 100644 --- a/modules/jooby-apt/src/test/java/tests/i3853/C3853.java +++ b/modules/jooby-apt/src/test/java/tests/i3853/C3853.java @@ -15,8 +15,7 @@ @Path("/3854") public class C3853 { - @GET("/stub") - @Project("(id, name)") + @GET(value = "/stub", projection = "(id, name)") public U3853 projectUser() { return new U3853(1, "Projected User", "Projected", "User"); } @@ -39,4 +38,10 @@ public Projected projected() { return Projected.wrap(new U3853(1, "Projected User", "Projected", "User")) .include("(id, name)"); } + + @GET(value = "/list", projection = "(id, name)") + public Projected projectedProjection() { + return Projected.wrap(new U3853(1, "Projected User", "Projected", "User")) + .include("(id, name)"); + } } diff --git a/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java b/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java index 9ed33d7293..b280bdd0ae 100644 --- a/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java +++ b/modules/jooby-apt/src/test/java/tests/i3853/Issue3853.java @@ -5,6 +5,8 @@ */ package tests.i3853; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,9 +18,9 @@ public void shouldSupportNameAttribute() throws Exception { new ProcessorRunner(new C3853()) .withSourceCode( source -> { - Assertions.assertTrue( - source.contains( - "return io.jooby.Projected.wrap(c.projectUser()).include(\"(id, name)\");")); + assertThat(source) + .contains( + "return io.jooby.Projected.wrap(c.projectUser()).include(\"(id, name)\");"); Assertions.assertTrue( source.contains( "return io.jooby.Projected.wrap(c.findUser()).include(\"(id, name)\");")); @@ -26,6 +28,7 @@ public void shouldSupportNameAttribute() throws Exception { source.contains( "return io.jooby.Projected.wrap(c.findUsers()).include(\"(id, name)\");")); Assertions.assertTrue(source.contains("return c.projected();")); + Assertions.assertTrue(source.contains("return c.projectedProjection();")); }) .withSourceCode( true, @@ -40,6 +43,7 @@ public void shouldSupportNameAttribute() throws Exception { source.contains( "return io.jooby.Projected.wrap(c.findUsers()).include(\"(id, name)\")")); Assertions.assertTrue(source.contains("return c.projected()")); + Assertions.assertTrue(source.contains("return c.projectedProjection()")); }); } } diff --git a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java index 9fc7ebad4a..e5e10ef7ff 100644 --- a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java +++ b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java @@ -8,8 +8,6 @@ import java.io.InputStream; import java.lang.reflect.Type; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import edu.umd.cs.findbugs.annotations.NonNull; import io.avaje.json.JsonWriter; @@ -65,8 +63,6 @@ */ public class AvajeJsonbModule implements Extension, MessageDecoder, MessageEncoder { - private final ConcurrentMap> viewCache = new ConcurrentHashMap<>(); - private final Jsonb jsonb; /** @@ -121,34 +117,30 @@ public Output encode(@NonNull Context ctx, @NonNull Object value) { @SuppressWarnings("unchecked") private void encodeProjection(JsonWriter writer, Projected projected) { - // Generate the Avaje-compatible view string (e.g., "(id,name,address(city))") var value = projected.getValue(); - var projection = projected.getProjection(); - var viewString = projection.toView(); - var type = projection.getType(); - var view = - (JsonView) - viewCache.computeIfAbsent( - value.getClass().getName() + viewString, - k -> { - var jsonbType = jsonb.type(type); - jsonbType = - switch (value) { - case Set ignored -> jsonbType.set(); - case Collection ignored -> jsonbType.list(); - default -> jsonbType; - }; - return jsonbType.view(viewString); - }); if (value instanceof Optional optional) { if (optional.isEmpty()) { writer.serializeNulls(true); writer.nullValue(); - } else { - view.toJson(optional.get(), writer); + return; } - } else { - view.toJson(value, writer); + value = optional.get(); + } + if (value instanceof Collection collection && collection.isEmpty()) { + writer.emptyArray(); + return; } + var projection = projected.getProjection(); + var viewString = projection.toView(); + var type = projection.getType(); + var jsonbType = jsonb.type(type); + jsonbType = + switch (value) { + case Set ignored -> jsonbType.set(); + case Collection ignored -> jsonbType.list(); + default -> jsonbType; + }; + var view = (JsonView) jsonbType.view(viewString); + view.toJson(value, writer); } } diff --git a/tests/src/test/java/io/jooby/i3853/A3853.java b/tests/src/test/java/io/jooby/i3853/A3853.java new file mode 100644 index 0000000000..9fc02a142e --- /dev/null +++ b/tests/src/test/java/io/jooby/i3853/A3853.java @@ -0,0 +1,27 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3853; + +import io.avaje.jsonb.Json; + +@Json +public class A3853 { + private final String city; + private final L3853 loc; + + public A3853(String city, L3853 loc) { + this.city = city; + this.loc = loc; + } + + public String getCity() { + return city; + } + + public L3853 getLoc() { + return loc; + } +} diff --git a/tests/src/test/java/io/jooby/i3853/C3853.java b/tests/src/test/java/io/jooby/i3853/C3853.java new file mode 100644 index 0000000000..234f5f4381 --- /dev/null +++ b/tests/src/test/java/io/jooby/i3853/C3853.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3853; + +import java.util.List; +import java.util.Optional; + +import io.jooby.Projected; +import io.jooby.annotation.GET; +import io.jooby.annotation.Path; +import io.jooby.annotation.Project; + +@Path("/3853") +public class C3853 { + @GET(value = "/stub", projection = "(id, name)") + public U3853 projectUser() { + return U3853.createUser(); + } + + @GET("/optional") + @Project("(id, name)") + public Optional findUser() { + return Optional.of(U3853.createUser()); + } + + @GET("/list") + @Project("(id)") + public List findUsers() { + return List.of(U3853.createUser()); + } + + @GET("/projected") + @Project("(id, name)") + public Projected projected() { + return Projected.wrap(U3853.createUser()).include("(id, name)"); + } + + @GET(value = "/projectedProjection", projection = "(id, name)") + public Projected projectedProjection() { + return Projected.wrap(U3853.createUser()).include("(id, name)"); + } +} diff --git a/tests/src/test/java/io/jooby/i3853/Issue3853.java b/tests/src/test/java/io/jooby/i3853/Issue3853.java index 89675bfc26..92d434a559 100644 --- a/tests/src/test/java/io/jooby/i3853/Issue3853.java +++ b/tests/src/test/java/io/jooby/i3853/Issue3853.java @@ -5,11 +5,11 @@ */ package io.jooby.i3853; +import static io.jooby.i3853.U3853.createUser; import static org.assertj.core.api.Assertions.assertThat; import java.util.*; -import io.avaje.jsonb.Json; import io.jooby.Extension; import io.jooby.Projected; import io.jooby.Projection; @@ -21,7 +21,7 @@ public class Issue3853 { - Projection STUB = Projection.of(User.class).include("(id, name)"); + Projection STUB = Projection.of(U3853.class).include("(id, name)"); @ServerTest public void shouldProjectJackson2Data(ServerTestRunner runner) { @@ -56,6 +56,12 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { return Projected.wrap(List.of(createUser())).include("(id, name)"); }); + app.get( + "/stub-empty-list", + ctx -> { + return Projected.wrap(List.of()).include("(id, name)"); + }); + app.get( "/stub-set", ctx -> { @@ -100,8 +106,8 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { "/stub/address-stub-ref", ctx -> { return Projected.wrap(createUser()) - .include(User::getId, User::getName) - .include(User::getAddress, addr -> addr.include(Address::getCity)); + .include(U3853::getId, U3853::getName) + .include(U3853::getAddress, addr -> addr.include(A3853::getCity)); }); }) .ready( @@ -151,6 +157,15 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { {"id":"cobb-001","name":"Dom Cobb"} """); }); + http.get( + "/stub-empty-list", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + [] + """); + }); http.get( "/stub-optional-null", rsp -> { @@ -239,92 +254,4 @@ public void jacksonShouldNotThrowInvalidDefinitionException( }); }); } - - @Json - public static class User { - private final String id; - private final String name; - private final Address address; - private final List roles; - private final Map meta; - - public User( - String id, String name, Address address, List roles, Map meta) { - this.id = id; - this.name = name; - this.address = address; - this.roles = roles; - this.meta = meta; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public Address getAddress() { - return address; - } - - public List getRoles() { - return roles; - } - - public Map getMeta() { - return meta; - } - } - - @Json - public static class Address { - private final String city; - private final Location loc; - - public Address(String city, Location loc) { - this.city = city; - this.loc = loc; - } - - public String getCity() { - return city; - } - - public Location getLoc() { - return loc; - } - } - - @Json - public record Role(String name, int level) {} - - @Json - public record Location(double lat, double lon) {} - - public static User createUser() { - // Nested Location: The Fortress in the Snow (Level 3) - Location fortress = new Location(80.0, -20.0); - - // Address: Represents the "Dream Layer" - Address dreamLayer = new Address("Snow Fortress (Level 3)", fortress); - - // Roles: The Extraction Team - List roles = - List.of( - new Role("The Extractor", 10), - new Role("The Architect", 9), - new Role("The Point Man", 8), - new Role("The Forger", 8)); - - // Metadata: Mission specs - Map meta = new LinkedHashMap<>(); - meta.put("target", "Robert Fischer"); - meta.put("objective", "Inception"); - meta.put("status", "Synchronizing Kicks"); - - // Root User: Dom Cobb - return new User("cobb-001", "Dom Cobb", dreamLayer, roles, meta); - } } diff --git a/tests/src/test/java/io/jooby/i3853/Issue3853Mvc.java b/tests/src/test/java/io/jooby/i3853/Issue3853Mvc.java new file mode 100644 index 0000000000..b563a088dd --- /dev/null +++ b/tests/src/test/java/io/jooby/i3853/Issue3853Mvc.java @@ -0,0 +1,91 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3853; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.jooby.Extension; +import io.jooby.avaje.jsonb.AvajeJsonbModule; +import io.jooby.jackson.JacksonModule; +import io.jooby.jackson3.Jackson3Module; +import io.jooby.junit.ServerTest; +import io.jooby.junit.ServerTestRunner; + +public class Issue3853Mvc { + + @ServerTest + public void shouldProjectJackson2Data(ServerTestRunner runner) { + shouldProjectData(runner, new JacksonModule()); + } + + @ServerTest + public void shouldProjectJackson3Data(ServerTestRunner runner) { + shouldProjectData(runner, new Jackson3Module()); + } + + @ServerTest + public void shouldProjectAvajeData(ServerTestRunner runner) { + shouldProjectData(runner, new AvajeJsonbModule()); + } + + public void shouldProjectData(ServerTestRunner runner, Extension extension) { + runner + .define( + app -> { + app.install(extension); + + app.mvc(new C3853_()); + }) + .ready( + http -> { + http.get( + "/3853/stub", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + {"id":"cobb-001","name":"Dom Cobb"} + """); + }); + http.get( + "/3853/optional", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + {"id":"cobb-001","name":"Dom Cobb"} + """); + }); + http.get( + "/3853/list", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + [{"id":"cobb-001"}] + """); + }); + http.get( + "/3853/projected", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + {"id":"cobb-001","name":"Dom Cobb"} + """); + }); + http.get( + "/3853/projectedProjection", + rsp -> { + assertThat(rsp.body().string()) + .isEqualToIgnoringNewLines( + """ + {"id":"cobb-001","name":"Dom Cobb"} + """); + }); + }); + } +} diff --git a/tests/src/test/java/io/jooby/i3853/L3853.java b/tests/src/test/java/io/jooby/i3853/L3853.java new file mode 100644 index 0000000000..918c6b79f5 --- /dev/null +++ b/tests/src/test/java/io/jooby/i3853/L3853.java @@ -0,0 +1,11 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3853; + +import io.avaje.jsonb.Json; + +@Json +public record L3853(double lat, double lon) {} diff --git a/tests/src/test/java/io/jooby/i3853/R3853.java b/tests/src/test/java/io/jooby/i3853/R3853.java new file mode 100644 index 0000000000..52fb11294b --- /dev/null +++ b/tests/src/test/java/io/jooby/i3853/R3853.java @@ -0,0 +1,11 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3853; + +import io.avaje.jsonb.Json; + +@Json +public record R3853(String name, int level) {} diff --git a/tests/src/test/java/io/jooby/i3853/U3853.java b/tests/src/test/java/io/jooby/i3853/U3853.java new file mode 100644 index 0000000000..45b66e1644 --- /dev/null +++ b/tests/src/test/java/io/jooby/i3853/U3853.java @@ -0,0 +1,74 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3853; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.avaje.jsonb.Json; + +@Json +public class U3853 { + private final String id; + private final String name; + private final A3853 address; + private final List roles; + private final Map meta; + + public U3853(String id, String name, A3853 address, List roles, Map meta) { + this.id = id; + this.name = name; + this.address = address; + this.roles = roles; + this.meta = meta; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public A3853 getAddress() { + return address; + } + + public List getRoles() { + return roles; + } + + public Map getMeta() { + return meta; + } + + public static U3853 createUser() { + // Nested Location: The Fortress in the Snow (Level 3) + L3853 fortress = new L3853(80.0, -20.0); + + // Address: Represents the "Dream Layer" + A3853 dreamLayer = new A3853("Snow Fortress (Level 3)", fortress); + + // Roles: The Extraction Team + List roles = + List.of( + new R3853("The Extractor", 10), + new R3853("The Architect", 9), + new R3853("The Point Man", 8), + new R3853("The Forger", 8)); + + // Metadata: Mission specs + Map meta = new LinkedHashMap<>(); + meta.put("target", "Robert Fischer"); + meta.put("objective", "Inception"); + meta.put("status", "Synchronizing Kicks"); + + // Root User: Dom Cobb + return new U3853("cobb-001", "Dom Cobb", dreamLayer, roles, meta); + } +} From d7c7946fe5cff7850ba454179ac74ca247c072d4 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Fri, 27 Feb 2026 18:30:50 -0300 Subject: [PATCH 23/31] - implement `@Project`/`projection = ""` for OpenAPI - ref #3853 --- jooby/src/main/java/io/jooby/Projection.java | 8 + .../jooby/internal/openapi/ClassSource.java | 11 - .../jooby/internal/openapi/OperationExt.java | 17 + .../jooby/internal/openapi/ParserContext.java | 11 +- .../openapi/projection/ProjectionParser.java | 67 +++ .../openapi/projection/SchemaPruner.java | 139 +++++++ .../io/jooby/openapi/OpenAPIGenerator.java | 3 + .../src/test/java/issues/i3853/A3853.java | 24 ++ .../src/test/java/issues/i3853/App3853.java | 16 + .../src/test/java/issues/i3853/C3853.java | 44 ++ .../src/test/java/issues/i3853/Issue3853.java | 380 ++++++++++++++++++ .../src/test/java/issues/i3853/L3853.java | 8 + .../src/test/java/issues/i3853/R3853.java | 8 + .../src/test/java/issues/i3853/U3853.java | 100 +++++ 14 files changed, 819 insertions(+), 17 deletions(-) create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/A3853.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/App3853.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/C3853.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/L3853.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/R3853.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/U3853.java diff --git a/jooby/src/main/java/io/jooby/Projection.java b/jooby/src/main/java/io/jooby/Projection.java index 099cb7c582..8f913136ed 100644 --- a/jooby/src/main/java/io/jooby/Projection.java +++ b/jooby/src/main/java/io/jooby/Projection.java @@ -15,6 +15,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import io.jooby.value.ValueFactory; + /** * Hierarchical schema for JSON field selection. A Projection defines exactly which fields of a Java * object should be serialized to JSON. @@ -203,6 +205,12 @@ public Projection validate() { return this; } + /** Determines if a type is a simple/scalar value that cannot be further projected. */ + private boolean isSimpleType(Type type) { + var valueFactory = new ValueFactory(); + return valueFactory.get(type) != null; + } + /** * Returns the Avaje-compatible DSL string. * diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ClassSource.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ClassSource.java index 375b632b2c..e1597ea654 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ClassSource.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ClassSource.java @@ -5,8 +5,6 @@ */ package io.jooby.internal.openapi; -import java.io.FileNotFoundException; -import java.io.IOException; import java.io.InputStream; import io.jooby.SneakyThrows; @@ -33,13 +31,4 @@ public byte[] loadClass(String classname) { throw SneakyThrows.propagate(x); } } - - public byte[] loadResource(String path) throws IOException { - try (InputStream stream = classLoader.getResourceAsStream(path)) { - if (stream == null) { - throw new FileNotFoundException(path); - } - return stream.readAllBytes(); - } - } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java index f04c8d3ab2..885ada1fdf 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java @@ -232,6 +232,23 @@ public List getAllAnnotations() { .collect(Collectors.toList()); } + @JsonIgnore + public String getProjection() { + return getAllAnnotations().stream() + .filter( + it -> + Router.METHODS.stream() + .map(method -> "Lio/jooby/annotation/" + method + ";") + .anyMatch(it.desc::equals) + && it.values != null) + .map(it -> AnnotationUtils.findAnnotationValue(it, "projection")) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(Object::toString) + .orElse(null); + } + public OperationExt copy(String pattern) { OperationExt copy = new OperationExt(node, method, pattern, getParameters(), defaultResponse); copy.setTags(getTags()); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java index f9ccb3102e..f88c8e79e2 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java @@ -12,7 +12,6 @@ import static java.util.Arrays.asList; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.Reader; @@ -427,10 +426,14 @@ public Schema schema(String type) { if (schema != null) { return schema.toSchema(); } + return schema(javaType(type)); + } + + public JavaType javaType(String type) { String json = "{\"type\":\"" + type + "\"}"; try { TypeLiteral literal = json().readValue(json, TypeLiteral.class); - return schema(literal.type); + return literal.type; } catch (Exception x) { throw SneakyThrows.propagate(x); } @@ -501,10 +504,6 @@ public ClassNode classNodeOrNull(Type type) { } } - public byte[] loadResource(String path) throws IOException { - return source.loadResource(path); - } - private ClassNode newClassNode(Type type) { ClassReader reader = new ClassReader(source.loadClass(type.getClassName())); if (debug.contains(DebugOption.ALL)) { diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java new file mode 100644 index 0000000000..4a1c51eb68 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java @@ -0,0 +1,67 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.projection; + +import static io.jooby.internal.openapi.AsmUtils.*; + +import io.jooby.Projection; +import io.jooby.annotation.Project; +import io.jooby.internal.openapi.OpenAPIExt; +import io.jooby.internal.openapi.OperationExt; +import io.jooby.internal.openapi.ParserContext; +import io.jooby.value.ValueFactory; +import io.swagger.v3.oas.models.media.Schema; + +public class ProjectionParser { + private OpenAPIExt openapi; + private ParserContext ctx; + + private ProjectionParser(ParserContext ctx, OpenAPIExt openapi) { + this.ctx = ctx; + this.openapi = openapi; + } + + public static void parse(ParserContext ctx, OpenAPIExt openapi) { + var parser = new ProjectionParser(ctx, openapi); + for (OperationExt operation : openapi.getOperations()) { + parser.parseOperation(operation); + } + } + + private void parseOperation(OperationExt operation) { + var annotations = operation.getAllAnnotations(); + + var projection = operation.getProjection(); + if (projection != null) { + projection(operation, projection); + } else { + findAnnotationByType(annotations, Project.class).stream() + .map(it -> stringValue(toMap(it), "value")) + .forEach(projectionView -> projection(operation, projectionView)); + } + } + + private void projection(OperationExt operation, String viewString) { + var response = operation.getDefaultResponse(); + var javaType = ctx.javaType(response.getJavaType()); + if (javaType.isArrayType() || javaType.isCollectionLikeType()) { + javaType = javaType.getContentType(); + } + var valueFactory = new ValueFactory(); + var isSimple = valueFactory.get(javaType) != null; + if (isSimple) { + return; + } + var projection = Projection.of(javaType.getRawClass()).include(viewString); + var content = response.getContent(); + for (var mediaTypes : content.entrySet()) { + Schema prune = + SchemaPruner.prune( + mediaTypes.getValue().getSchema(), projection, openapi.getComponents()); + mediaTypes.getValue().setSchema(prune); + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java new file mode 100644 index 0000000000..4179f22c15 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java @@ -0,0 +1,139 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.projection; + +import java.util.List; +import java.util.Map; + +import io.jooby.Projection; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; + +/** + * Utility to create a pruned OpenAPI Schema based on a Jooby Projection. + * + * @since 4.0.0 + */ +public class SchemaPruner { + + public static Schema prune( + Schema original, Projection projection, Components components) { + // 1. Deep wildcard (e.g., address(*)). Return the original schema/ref untouched. + if (projection.getChildren().isEmpty()) { + return original; + } + + // 2. Handle Arrays: Recursively prune the items. + if (original instanceof ArraySchema) { + ArraySchema arraySchema = (ArraySchema) original; + Schema prunedItems = prune(arraySchema.getItems(), projection, components); + + ArraySchema newArraySchema = new ArraySchema(); + copyMetadata(original, newArraySchema); + newArraySchema.setItems(prunedItems); + return newArraySchema; + } + + // --- THE CACHE CHECK (Early Exit) --- + String baseName = getBaseName(original); + String newComponentName = null; + + if (baseName != null && components != null) { + newComponentName = generateProjectedName(baseName, projection); + + // If we already built this exact projection view, reuse it immediately! + if (components.getSchemas() != null + && components.getSchemas().containsKey(newComponentName)) { + return new Schema<>().$ref("#/components/schemas/" + newComponentName); + } + } + // ------------------------------------ + + // 3. Resolve the actual properties from the original schema + Schema actualSchema = resolveSchema(original, components); + if (actualSchema == null || actualSchema.getProperties() == null) { + return original; + } + + // 4. Build the new pruned ObjectSchema + Schema prunedSchema = new ObjectSchema(); + copyMetadata(actualSchema, prunedSchema); + Map originalProps = actualSchema.getProperties(); + + for (Map.Entry> entry : projection.getChildren().entrySet()) { + String propName = entry.getKey(); + Projection childNode = entry.getValue(); + Schema originalPropSchema = originalProps.get(propName); + + if (originalPropSchema != null) { + // Recursion naturally handles leaf nodes vs. nested objects + Schema prunedProp = prune(originalPropSchema, childNode, components); + prunedSchema.addProperty(propName, prunedProp); + } + } + + // 5. Componentize! Register the new one and return a $ref. + if (newComponentName != null && components != null) { + // Add the fully built pruned schema to the global registry + components.addSchemas(newComponentName, prunedSchema); + + // Return a lightweight $ref pointer to the newly registered component + return new Schema<>().$ref("#/components/schemas/" + newComponentName); + } + + // Fallback: If it was an anonymous inline object to begin with, just return the inline pruned + // object. + return prunedSchema; + } + + private static Schema resolveSchema(Schema schema, Components components) { + if (schema.get$ref() != null && components != null && components.getSchemas() != null) { + String refName = schema.get$ref().substring(schema.get$ref().lastIndexOf('/') + 1); + return components.getSchemas().get(refName); + } + return schema; + } + + private static String getBaseName(Schema schema) { + if (schema.get$ref() != null) { + return schema.get$ref().substring(schema.get$ref().lastIndexOf('/') + 1); + } + return schema.getName(); + } + + private static String generateProjectedName(String baseName, Projection projection) { + String shortHash = Integer.toString(Math.abs(projection.toView().hashCode()), 36); + return baseName + "_" + shortHash; + } + + private static void copyMetadata(Schema source, Schema target) { + target.setName(source.getName()); + target.setTitle(source.getTitle()); + target.setDescription(source.getDescription()); + target.setFormat(source.getFormat()); + target.setDefault(source.getDefault()); + if (source.getExample() != null) { + target.setExample(source.getExample()); + } + target.setEnum((List) source.getEnum()); + target.setRequired(source.getRequired()); + + target.setMaximum(source.getMaximum()); + target.setMinimum(source.getMinimum()); + target.setMaxLength(source.getMaxLength()); + target.setMinLength(source.getMinLength()); + target.setPattern(source.getPattern()); + target.setMaxItems(source.getMaxItems()); + target.setMinItems(source.getMinItems()); + target.setUniqueItems(source.getUniqueItems()); + + if (source.getExtensions() != null) { + source.getExtensions().forEach(target::addExtension); + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java index 8eaf5a02ec..31b4189f13 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java @@ -23,6 +23,7 @@ import io.jooby.internal.openapi.*; import io.jooby.internal.openapi.asciidoc.AsciiDocContext; import io.jooby.internal.openapi.javadoc.JavaDocParser; +import io.jooby.internal.openapi.projection.ProjectionParser; import io.swagger.v3.core.util.*; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.PathItem; @@ -318,6 +319,8 @@ public OpenAPIGenerator() {} openapi.setJsonSchemaDialect(null); } + ProjectionParser.parse(ctx, openapi); + return openapi; } diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/A3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/A3853.java new file mode 100644 index 0000000000..0d8bb754cb --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/A3853.java @@ -0,0 +1,24 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +public class A3853 { + private final String city; + private final L3853 loc; + + public A3853(String city, L3853 loc) { + this.city = city; + this.loc = loc; + } + + public String getCity() { + return city; + } + + public L3853 getLoc() { + return loc; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/App3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/App3853.java new file mode 100644 index 0000000000..7e0a285093 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/App3853.java @@ -0,0 +1,16 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + +import io.jooby.Jooby; + +public class App3853 extends Jooby { + { + mvc(toMvcExtension(C3853.class)); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/C3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/C3853.java new file mode 100644 index 0000000000..d656b0f53e --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/C3853.java @@ -0,0 +1,44 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +import java.util.List; +import java.util.Optional; + +import io.jooby.annotation.*; + +@Path("/3853") +public class C3853 { + + @GET("/{id}") + @Project("(id, name)") + public U3853 findUser(@PathParam String id) { + return null; + } + + @GET(value = "/", projection = "(id, name)") + public List findUsers() { + return null; + } + + @GET("/optional") + @Project("(id)") + public Optional findUserIdOnly() { + return null; + } + + @GET("/full-address/{id}") + @Project("(id, address(*))") + public U3853 userIdWithFullAddress(@PathParam String id) { + return null; + } + + @GET("/partial-address/{id}") + @Project("(id, address(city))") + public U3853 userIdWithAddressCity(@PathParam String id) { + return null; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java new file mode 100644 index 0000000000..b36078b95c --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java @@ -0,0 +1,380 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import io.jooby.openapi.OpenAPIResult; +import io.jooby.openapi.OpenAPITest; +import io.swagger.v3.oas.models.SpecVersion; + +public class Issue3853 { + @OpenAPITest(value = App3853.class) + public void shouldDetectProjectAnnotation(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.0.1 + info: + title: 3853 API + description: 3853 API description + version: "1.0" + paths: + /3853/{id}: + get: + operationId: findUser + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_heab3f" + /3853: + get: + operationId: findUsers + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/optional: + get: + operationId: findUserIdOnly + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_2l7" + /3853/full-address/{id}: + get: + operationId: userIdWithFullAddress + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_ff52rt" + /3853/partial-address/{id}: + get: + operationId: userIdWithAddressCity + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_xsv0o" + components: + schemas: + U3853: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + address: + $ref: "#/components/schemas/A3853" + roles: + type: array + description: Retrieves the list of roles associated with the user. + items: + $ref: "#/components/schemas/R3853" + meta: + type: object + additionalProperties: + type: string + description: Retrieves the metadata associated with the user. + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + L3853: + type: object + properties: + lat: + type: number + format: double + lon: + type: number + format: double + R3853: + type: object + properties: + name: + type: string + level: + type: integer + format: int32 + A3853: + type: object + properties: + city: + type: string + loc: + $ref: "#/components/schemas/L3853" + U3853_heab3f: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + U3853_2l7: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + U3853_ff52rt: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853" + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + A3853_1tgff: + type: object + properties: + city: + type: string + U3853_xsv0o: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853_1tgff" + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + """); + } + + @OpenAPITest(value = App3853.class, version = SpecVersion.V31) + public void shouldDetectProjectAnnotationV31(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.1.0 + info: + title: 3853 API + description: 3853 API description + version: "1.0" + paths: + /3853/{id}: + get: + operationId: findUser + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_heab3f" + /3853: + get: + operationId: findUsers + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/optional: + get: + operationId: findUserIdOnly + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_2l7" + /3853/full-address/{id}: + get: + operationId: userIdWithFullAddress + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_ff52rt" + /3853/partial-address/{id}: + get: + operationId: userIdWithAddressCity + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_xsv0o" + components: + schemas: + U3853: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + address: + $ref: "#/components/schemas/A3853" + description: Retrieves the address associated with the user. + roles: + type: array + description: Retrieves the list of roles associated with the user. + items: + $ref: "#/components/schemas/R3853" + meta: + type: object + additionalProperties: + type: string + description: Retrieves the metadata associated with the user. + L3853: + type: object + properties: + lat: + type: number + format: double + lon: + type: number + format: double + R3853: + type: object + properties: + name: + type: string + level: + type: integer + format: int32 + A3853: + type: object + properties: + city: + type: string + loc: + $ref: "#/components/schemas/L3853" + U3853_heab3f: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + U3853_2l7: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + U3853_ff52rt: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853" + description: Retrieves the address associated with the user. + A3853_1tgff: + type: object + properties: + city: + type: string + U3853_xsv0o: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853_1tgff" + """); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/L3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/L3853.java new file mode 100644 index 0000000000..aab676e966 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/L3853.java @@ -0,0 +1,8 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +public record L3853(double lat, double lon) {} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/R3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/R3853.java new file mode 100644 index 0000000000..d5f1407b1f --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/R3853.java @@ -0,0 +1,8 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +public record R3853(String name, int level) {} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/U3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/U3853.java new file mode 100644 index 0000000000..846f136ac1 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/U3853.java @@ -0,0 +1,100 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a user entity identified by an ID and name, with associated address details, roles, + * and metadata. This class is immutable, ensuring the integrity of its fields. + */ +public class U3853 { + private final String id; + private final String name; + private final A3853 address; + private final List roles; + private final Map meta; + + public U3853(String id, String name, A3853 address, List roles, Map meta) { + this.id = id; + this.name = name; + this.address = address; + this.roles = roles; + this.meta = meta; + } + + /** + * Retrieves the unique identifier for the user. + * + * @return the user ID as a string. + */ + public String getId() { + return id; + } + + /** + * Retrieves the name of the user. + * + * @return the name as a string. + */ + public String getName() { + return name; + } + + /** + * Retrieves the address associated with the user. + * + * @return the user's address as an instance of A3853. + */ + public A3853 getAddress() { + return address; + } + + /** + * Retrieves the list of roles associated with the user. + * + * @return a list of R3853 instances representing the roles of the user. + */ + public List getRoles() { + return roles; + } + + /** + * Retrieves the metadata associated with the user. + * + * @return a map containing metadata as key-value pairs, where both keys and values are strings. + */ + public Map getMeta() { + return meta; + } + + public static U3853 createUser() { + // Nested Location: The Fortress in the Snow (Level 3) + L3853 fortress = new L3853(80.0, -20.0); + + // Address: Represents the "Dream Layer" + A3853 dreamLayer = new A3853("Snow Fortress (Level 3)", fortress); + + // Roles: The Extraction Team + List roles = + List.of( + new R3853("The Extractor", 10), + new R3853("The Architect", 9), + new R3853("The Point Man", 8), + new R3853("The Forger", 8)); + + // Metadata: Mission specs + Map meta = new LinkedHashMap<>(); + meta.put("target", "Robert Fischer"); + meta.put("objective", "Inception"); + meta.put("status", "Synchronizing Kicks"); + + // Root User: Dom Cobb + return new U3853("cobb-001", "Dom Cobb", dreamLayer, roles, meta); + } +} From db563bfac036ded4bba802f1390a429ac14c5aeb Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Feb 2026 14:08:47 -0300 Subject: [PATCH 24/31] - implementa `Projected` on script routes - ref #3853 --- .../io/jooby/internal/openapi/OpenAPIExt.java | 23 +- .../jooby/internal/openapi/OperationExt.java | 5 + .../jooby/internal/openapi/ParserContext.java | 15 +- .../jooby/internal/openapi/RouteParser.java | 2 +- .../openapi/asciidoc/AsciiDocContext.java | 10 +- .../projection/AsmProjectionParser.java | 308 +++++++++++++ .../openapi/projection/ProjectionParser.java | 78 +++- .../openapi/projection/SchemaPruner.java | 46 +- .../io/jooby/openapi/OpenAPIGenerator.java | 16 +- .../test/java/issues/i3853/App3853Script.java | 79 ++++ .../src/test/java/issues/i3853/Issue3853.java | 403 ++++++++++++++++++ 11 files changed, 926 insertions(+), 59 deletions(-) create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/AsmProjectionParser.java create mode 100644 modules/jooby-openapi/src/test/java/issues/i3853/App3853Script.java diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java index 0004a40ab1..8e10b49b49 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIExt.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import io.jooby.Router; import io.swagger.v3.oas.models.*; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.security.SecurityScheme; @@ -39,12 +40,24 @@ public void setSource(String classname) { } public void addSecuritySchemes(String name, SecurityScheme scheme) { - var components = getComponents(); - if (components == null) { - components = new Components(); - setComponents(components); + getRequiredComponents().addSecuritySchemes(name, scheme); + } + + @JsonIgnore + public Components getRequiredComponents() { + if (getComponents() == null) { + setComponents(new Components()); + } + return getComponents(); + } + + @JsonIgnore + public Map getRequiredSchemas() { + var components = getRequiredComponents(); + if (components.getSchemas() == null) { + components.setSchemas(new LinkedHashMap<>()); } - components.addSecuritySchemes(name, scheme); + return components.getSchemas(); } @Override diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java index 885ada1fdf..55436e68af 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java @@ -292,4 +292,9 @@ public OperationExt copy(String pattern) { public String getPath(Map pathParams) { return Router.reverse(getPath(), pathParams); } + + @JsonIgnore + public boolean isScript() { + return getController() == null; + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java index f88c8e79e2..a699d77b55 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java @@ -53,10 +53,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import io.jooby.Context; -import io.jooby.FileUpload; -import io.jooby.SneakyThrows; -import io.jooby.StatusCode; +import io.jooby.*; import io.jooby.internal.openapi.javadoc.JavaDocParser; import io.jooby.openapi.DebugOption; import io.swagger.v3.core.util.RefUtils; @@ -172,6 +169,9 @@ public Schema schema(Class type) { if (isVoid(type.getName())) { return null; } + if (type == Projected.class) { + return new ObjectSchema().name("Projected"); + } if (type == String.class) { return new StringSchema(); } @@ -496,6 +496,13 @@ public MethodNode findMethodNode(Type type, String name) { .orElseThrow(() -> new IllegalArgumentException("Method not found: " + type + "." + name)); } + public MethodNode findMethodNode(Type type, String name, String desc) { + return nodes.computeIfAbsent(type, this::newClassNode).methods.stream() + .filter(it -> it.name.equals(name) && it.desc.equals(desc)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Method not found: " + type + "." + name)); + } + public ClassNode classNodeOrNull(Type type) { try { return nodes.computeIfAbsent(type, this::newClassNode); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java index 95d6c45ba5..ca5dc2ab1b 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java @@ -271,7 +271,7 @@ private String patternToOperationId(String pattern) { return ""; } return Stream.of(pattern.split("\\W+")) - .filter(s -> s.length() > 0) + .filter(s -> !s.isEmpty()) .map( segment -> Character.toUpperCase(segment.charAt(0)) diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java index c599382a7d..e4963682fb 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/asciidoc/AsciiDocContext.java @@ -163,11 +163,8 @@ public Map getGlobalVariables() { .toList(); openapiRoot.put("tags", tags); // Schemas - var components = context.openapi.getComponents(); - if (components != null && components.getSchemas() != null) { - var schemas = components.getSchemas(); - openapiRoot.put("schemas", new ArrayList<>(schemas.values())); - } + var schemas = context.openapi.getRequiredSchemas(); + openapiRoot.put("schemas", new ArrayList<>(schemas.values())); // make in to work without literal openapiRoot.put("query", "query"); @@ -466,9 +463,6 @@ public Schema resolveSchema(String path) { private Optional> resolveSchemaInternal(String name) { var components = openapi.getComponents(); - if (components == null || components.getSchemas() == null) { - throw new NoSuchElementException("No schema found"); - } if (name.startsWith(COMPONENTS_SCHEMAS_REF)) { name = name.substring(COMPONENTS_SCHEMAS_REF.length()); } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/AsmProjectionParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/AsmProjectionParser.java new file mode 100644 index 0000000000..59b8ac3788 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/AsmProjectionParser.java @@ -0,0 +1,308 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.projection; + +import java.util.Optional; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import io.jooby.internal.openapi.InsnSupport; +import io.jooby.internal.openapi.ParserContext; + +/** + * Parses bytecode to find Projected.wrap(...).include("...") definitions. Extracts the target class + * (with generics when available) and the view string. + */ +public class AsmProjectionParser { + + public static class ProjectionDef { + public final String targetClass; + public final String viewString; + + public ProjectionDef(String targetClass, String viewString) { + this.targetClass = targetClass; + this.viewString = viewString; + } + } + + /** + * Scans a method's instructions to find a Projection definition. + * + * @param methodNode The MethodNode containing the local variable table (LVTT). + * @return The parsed ProjectionDef, or empty if not found. + */ + public static Optional parse(ParserContext ctx, MethodNode methodNode) { + if (methodNode == null + || methodNode.instructions == null + || methodNode.instructions.size() == 0) { + return Optional.empty(); + } + + // Start from the first instruction in the method + AbstractInsnNode methodStart = methodNode.instructions.getFirst(); + + // 1. Find `Projected.include(String)` + MethodInsnNode includeNode = + (MethodInsnNode) + InsnSupport.next(methodStart) + .filter(n -> n.getOpcode() == Opcodes.INVOKEVIRTUAL) + .filter(n -> n instanceof MethodInsnNode) + .filter( + n -> { + MethodInsnNode mn = (MethodInsnNode) n; + return "io/jooby/Projected".equals(mn.owner) && "include".equals(mn.name); + }) + .findFirst() + .orElse(null); + + if (includeNode == null) { + return Optional.empty(); + } + + // 2. Extract View String (e.g., "(id, name)") + String viewString = + InsnSupport.prev(includeNode) + .filter(n -> n.getOpcode() == Opcodes.LDC && ((LdcInsnNode) n).cst instanceof String) + .map(n -> (String) ((LdcInsnNode) n).cst) + .findFirst() + .orElse(null); + + if (viewString == null) { + return Optional.empty(); + } + + // 3. Find `wrap(...)` and trace back to its payload + MethodInsnNode wrapNode = findWrapNode(includeNode); + String targetClass = null; + + if (wrapNode != null) { + AbstractInsnNode payloadNode = wrapNode.getPrevious(); + + // If it's a variable: `Projected.wrap(myList)` + if (payloadNode.getOpcode() == Opcodes.ALOAD) { + int varIndex = ((VarInsnNode) payloadNode).var; + + // Try to get the generic signature from the Local Variable Type Table (LVTT) + targetClass = extractFromLocalVariables(methodNode, varIndex); + + if (targetClass == null) { + // Fallback: trace to ASTORE if LVTT is missing or doesn't have generics + payloadNode = findAstore(wrapNode, varIndex); + } + } + + // If it wasn't a variable, or LVTT failed, fall back to instruction heuristics + if (targetClass == null && payloadNode != null) { + targetClass = extractTypeWithGenerics(ctx, payloadNode); + } + } + + return Optional.of(new ProjectionDef(targetClass, viewString)); + } + + // --- Data-Flow Helpers --- + + private static MethodInsnNode findWrapNode(AbstractInsnNode includeNode) { + AbstractInsnNode prev = includeNode.getPrevious(); + while (prev != null) { + if (isWrapCall(prev)) { + return (MethodInsnNode) prev; + } + if (prev.getOpcode() == Opcodes.ALOAD) { + int varIndex = ((VarInsnNode) prev).var; + AbstractInsnNode assignment = findAstore(prev, varIndex); + if (assignment != null && isWrapCall(assignment.getPrevious())) { + return (MethodInsnNode) assignment.getPrevious(); + } + } + prev = prev.getPrevious(); + } + return null; + } + + private static boolean isWrapCall(AbstractInsnNode n) { + if (n != null && n.getOpcode() == Opcodes.INVOKESTATIC && n instanceof MethodInsnNode) { + MethodInsnNode mn = (MethodInsnNode) n; + return "io/jooby/Projected".equals(mn.owner) && "wrap".equals(mn.name); + } + return false; + } + + private static AbstractInsnNode findAstore(AbstractInsnNode start, int varIndex) { + AbstractInsnNode prev = start.getPrevious(); + while (prev != null) { + if (prev.getOpcode() == Opcodes.ASTORE && ((VarInsnNode) prev).var == varIndex) { + return prev.getPrevious(); + } + prev = prev.getPrevious(); + } + return null; + } + + // --- Local Variable Type Table (LVTT) Resolution --- + + private static String extractFromLocalVariables(MethodNode methodNode, int varIndex) { + if (methodNode.localVariables == null) { + return null; + } + + for (LocalVariableNode lvn : methodNode.localVariables) { + if (lvn.index == varIndex) { + // Prefer the full generic signature if available + if (lvn.signature != null) { + return parseAsmSignature(lvn.signature); + } + // Fallback to the erased descriptor (e.g., Ljava/util/List;) + if (lvn.desc != null) { + return Type.getType(lvn.desc).getClassName(); + } + } + } + return null; + } + + /** + * Converts an ASM generic signature to a clean Java class name. Input: + * Ljava/util/List; Output: java.util.List + */ + private static String parseAsmSignature(String signature) { + if (signature == null) return null; + + // 1. Detect and erase unresolved Type Variables (like TE; or TId;) + // In JVM signatures, type variables always start with 'T', end with ';', + // and are immediately preceded by '<', ';', '[', or ')' + signature = signature.replaceAll("(?<=[<;\\[)])T[^;]+;", "Ljava/lang/Object;"); + + // 2. Convert slashes to dots + String cleaned = signature.replace('/', '.'); + + // 3. Strip the outermost 'L' and ';' + if (cleaned.startsWith("L")) { + cleaned = cleaned.substring(1); + } + if (cleaned.endsWith(";")) { + cleaned = cleaned.substring(0, cleaned.length() - 1); + } + + // 4. Handle multiple generic arguments (separated by ;L) + cleaned = cleaned.replace(";L", ", "); + + // 5. Strip the 'L' that appears right after the opening bracket + cleaned = cleaned.replace(" signature. + if (isContainerClass(className) && mn.getOpcode() == Opcodes.INVOKESTATIC) { + String genericParam = inferGenericArgument(mn); + if (genericParam != null) { + return className + "<" + genericParam + ">"; + } + } + + // --- 2. SECOND PRIORITY: Deep Method Signature Resolution --- + // If it's an instance method like service.list(), try to look up its generic + // return type using ParserContext. + if (ctx != null) { + try { + Type ownerType = Type.getObjectType(mn.owner); + MethodNode targetMethod = ctx.findMethodNode(ownerType, mn.name, mn.desc); + + if (targetMethod != null && targetMethod.signature != null) { + // Find the return type portion of the signature + int returnTypeStart = targetMethod.signature.lastIndexOf(')'); + if (returnTypeStart != -1) { + String returnSignature = targetMethod.signature.substring(returnTypeStart + 1); + String parsed = parseAsmSignature(returnSignature); + + // Only return this if it actually gave us something better than Object. + // If it just gave us List because of a erasure, we might + // as well fall through to the base class name anyway. + if (parsed != null && !parsed.contains("")) { + return parsed; + } + } + } + } catch (Exception e) { + // Ignore + } + } + + // --- 3. FALLBACK: Return the erased class name --- + if (!"java.lang.Object".equals(className)) { + return className; + } + } + return null; + } + + private static boolean isContainerClass(String className) { + return "java.util.List".equals(className) + || "java.util.Set".equals(className) + || "java.util.Collection".equals(className) + || "java.util.Optional".equals(className); + } + + /** + * Scans backwards to find the type of the argument passed to a container method. Useful for + * inline calls like: List.of(U3853.createUser()) + */ + private static String inferGenericArgument(MethodInsnNode methodCall) { + Type[] argTypes = Type.getArgumentTypes(methodCall.desc); + + if (argTypes.length == 0) { + return null; + } + + AbstractInsnNode prev = methodCall.getPrevious(); + + while (prev != null) { + if (prev.getOpcode() == Opcodes.NEW) { + return Type.getObjectType(((TypeInsnNode) prev).desc).getClassName(); + } + if (prev instanceof MethodInsnNode) { + MethodInsnNode argMethod = (MethodInsnNode) prev; + String returnClass = Type.getReturnType(argMethod.desc).getClassName(); + if (!"java.lang.Object".equals(returnClass)) { + return returnClass; + } + } + prev = prev.getPrevious(); + } + + return null; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java index 4a1c51eb68..529d10a483 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/ProjectionParser.java @@ -7,13 +7,17 @@ import static io.jooby.internal.openapi.AsmUtils.*; +import java.util.Map; + +import com.fasterxml.jackson.databind.JavaType; import io.jooby.Projection; import io.jooby.annotation.Project; import io.jooby.internal.openapi.OpenAPIExt; import io.jooby.internal.openapi.OperationExt; import io.jooby.internal.openapi.ParserContext; import io.jooby.value.ValueFactory; -import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.MediaType; public class ProjectionParser { private OpenAPIExt openapi; @@ -34,34 +38,80 @@ public static void parse(ParserContext ctx, OpenAPIExt openapi) { private void parseOperation(OperationExt operation) { var annotations = operation.getAllAnnotations(); - var projection = operation.getProjection(); - if (projection != null) { - projection(operation, projection); + if (operation.isScript()) { + AsmProjectionParser.parse(ctx, operation.getNode()) + .ifPresent( + projectionDef -> { + projection(operation, projectionDef.targetClass, projectionDef.viewString); + }); } else { - findAnnotationByType(annotations, Project.class).stream() - .map(it -> stringValue(toMap(it), "value")) - .forEach(projectionView -> projection(operation, projectionView)); + var projection = operation.getProjection(); + if (projection != null) { + projection(operation, projection); + } else { + findAnnotationByType(annotations, Project.class).stream() + .map(it -> stringValue(toMap(it), "value")) + .forEach(projectionView -> projection(operation, projectionView)); + } } } private void projection(OperationExt operation, String viewString) { + projection(operation, operation.getDefaultResponse().getJavaType(), viewString); + } + + private void projection(OperationExt operation, String responseType, String viewString) { + projection(operation, ctx.javaType(responseType), viewString); + } + + private void projection(OperationExt operation, JavaType responseType, String viewString) { var response = operation.getDefaultResponse(); - var javaType = ctx.javaType(response.getJavaType()); - if (javaType.isArrayType() || javaType.isCollectionLikeType()) { - javaType = javaType.getContentType(); + var contentType = responseType; + if (responseType.isArrayType() || responseType.isCollectionLikeType()) { + contentType = responseType.getContentType(); } var valueFactory = new ValueFactory(); - var isSimple = valueFactory.get(javaType) != null; + var isSimple = valueFactory.get(contentType) != null; if (isSimple) { return; } - var projection = Projection.of(javaType.getRawClass()).include(viewString); + if (operation.isScript()) { + prepareScript(operation, responseType); + } + var projection = Projection.of(contentType.getRawClass()).include(viewString); var content = response.getContent(); for (var mediaTypes : content.entrySet()) { - Schema prune = + var prune = SchemaPruner.prune( - mediaTypes.getValue().getSchema(), projection, openapi.getComponents()); + mediaTypes.getValue().getSchema(), projection, openapi.getRequiredComponents()); mediaTypes.getValue().setSchema(prune); } } + + private void prepareScript(OperationExt operation, JavaType responseType) { + var response = operation.getDefaultResponse(); + var contentType = + (responseType.isArrayType() || responseType.isCollectionLikeType()) + ? responseType.getContentType() + : responseType; + var schemas = openapi.getRequiredSchemas(); + var schema = + schemas.computeIfAbsent( + contentType.getRawClass().getSimpleName(), + schemaName -> { + // 1.Initialize + ctx.schema(contentType.getRawClass()); + //noinspection OptionalGetWithoutIsPresent + var schemaRef = ctx.schemaRef(contentType.getRawClass().getName()); + return schemaRef.get().schema; + }); + // Save schemas after projection in case a new one was created + ctx.schemas().forEach(it -> openapi.schema(it.getName(), it)); + if (responseType.isArrayType() || responseType.isCollectionLikeType()) { + schema = new ArraySchema().items(schema); + } + for (Map.Entry e : response.getContent().entrySet()) { + e.getValue().setSchema(schema); + } + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java index 4179f22c15..f54de228ff 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/projection/SchemaPruner.java @@ -23,6 +23,10 @@ public class SchemaPruner { public static Schema prune( Schema original, Projection projection, Components components) { + if (original == null || projection == null) { + return original; + } + // 1. Deep wildcard (e.g., address(*)). Return the original schema/ref untouched. if (projection.getChildren().isEmpty()) { return original; @@ -39,28 +43,28 @@ public static Schema prune( return newArraySchema; } + // --- AGGRESSIVE ROOT RESOLUTION --- + // We MUST resolve the actual object schema if 'original' is just a $ref pointer, + // otherwise we can't see the properties we need to prune. + Schema actualSchema = resolveSchema(original, components); + if (actualSchema == null || actualSchema.getProperties() == null) { + return original; + } + // --- THE CACHE CHECK (Early Exit) --- + // We use the base name of the $ref (if it had one) to name our new component. String baseName = getBaseName(original); String newComponentName = null; - if (baseName != null && components != null) { + if (baseName != null) { newComponentName = generateProjectedName(baseName, projection); - // If we already built this exact projection view, reuse it immediately! - if (components.getSchemas() != null - && components.getSchemas().containsKey(newComponentName)) { + if (components.getSchemas().containsKey(newComponentName)) { return new Schema<>().$ref("#/components/schemas/" + newComponentName); } } - // ------------------------------------ - // 3. Resolve the actual properties from the original schema - Schema actualSchema = resolveSchema(original, components); - if (actualSchema == null || actualSchema.getProperties() == null) { - return original; - } - - // 4. Build the new pruned ObjectSchema + // --- THE PRUNING --- Schema prunedSchema = new ObjectSchema(); copyMetadata(actualSchema, prunedSchema); Map originalProps = actualSchema.getProperties(); @@ -71,30 +75,30 @@ public static Schema prune( Schema originalPropSchema = originalProps.get(propName); if (originalPropSchema != null) { - // Recursion naturally handles leaf nodes vs. nested objects Schema prunedProp = prune(originalPropSchema, childNode, components); prunedSchema.addProperty(propName, prunedProp); } } - // 5. Componentize! Register the new one and return a $ref. + // --- REGISTER IN COMPONENTS CACHE --- if (newComponentName != null && components != null) { - // Add the fully built pruned schema to the global registry components.addSchemas(newComponentName, prunedSchema); - - // Return a lightweight $ref pointer to the newly registered component return new Schema<>().$ref("#/components/schemas/" + newComponentName); } - // Fallback: If it was an anonymous inline object to begin with, just return the inline pruned - // object. return prunedSchema; } + /** + * Resolves a $ref to its actual Schema object in the Components map. If the schema is not a $ref, + * it returns the schema itself. + */ private static Schema resolveSchema(Schema schema, Components components) { - if (schema.get$ref() != null && components != null && components.getSchemas() != null) { + if (schema.get$ref() != null) { + // Extract "User" from "#/components/schemas/User" String refName = schema.get$ref().substring(schema.get$ref().lastIndexOf('/') + 1); - return components.getSchemas().get(refName); + Schema resolved = components.getSchemas().get(refName); + return resolved != null ? resolved : schema; } return schema; } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java index 31b4189f13..67718cfdfa 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java @@ -25,10 +25,7 @@ import io.jooby.internal.openapi.javadoc.JavaDocParser; import io.jooby.internal.openapi.projection.ProjectionParser; import io.swagger.v3.core.util.*; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.Paths; -import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.*; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.oas.models.tags.Tag; @@ -263,8 +260,6 @@ public OpenAPIGenerator() {} defaults(classname, contextPath, openapi); - ctx.schemas().forEach(schema -> openapi.schema(schema.getName(), schema)); - Map globalTags = new LinkedHashMap<>(); var paths = new Paths(); for (var operation : operations) { @@ -319,11 +314,20 @@ public OpenAPIGenerator() {} openapi.setJsonSchemaDialect(null); } + // Put schemas so far + ctx.schemas().forEach(schema -> openapi.schema(schema.getName(), schema)); + ProjectionParser.parse(ctx, openapi); + // Save schemas after projection in case a new one was created + ctx.schemas().forEach(schema -> openapi.schema(schema.getName(), schema)); + + finish(openapi); return openapi; } + private void finish(OpenAPIExt openapi) {} + ObjectMapper yamlMapper() { return specVersion == SpecVersion.V30 ? Yaml.mapper() : Yaml31.mapper(); } diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/App3853Script.java b/modules/jooby-openapi/src/test/java/issues/i3853/App3853Script.java new file mode 100644 index 0000000000..e0a68f7300 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3853/App3853Script.java @@ -0,0 +1,79 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3853; + +import java.util.ArrayList; +import java.util.List; + +import io.jooby.Jooby; +import io.jooby.Projected; + +public class App3853Script extends Jooby { + public static class Service { + public List list() { + return null; + } + } + + { + get( + "/3853/{id}", + ctx -> { + var id = ctx.path("id").intValue(); + return Projected.wrap(U3853.createUser()).include("(id, name)"); + }); + + get( + "/3853/list-variable", + ctx -> { + List list = new ArrayList<>(); + list.add(U3853.createUser()); + return Projected.wrap(list).include("(id, name)"); + }); + + get( + "/3853/list-variable-var", + ctx -> { + var list = new ArrayList(); + list.add(U3853.createUser()); + return Projected.wrap(list).include("(id, name)"); + }); + + get( + "/3853/list-call", + ctx -> { + var service = ctx.require(Service.class); + return Projected.wrap(service.list()).include("(id, name)"); + }); + + get( + "/3853/list-call-variable", + ctx -> { + var service = ctx.require(Service.class); + var result = service.list(); + + return Projected.wrap(result).include("(id, name)"); + }); + + get( + "/3853/list", + ctx -> { + return Projected.wrap(List.of(U3853.createUser())).include("(id, name)"); + }); + + get( + "/3853/address", + ctx -> { + return Projected.wrap(U3853.createUser()).include("(id, address(*))"); + }); + + get( + "/3853/address/partial", + ctx -> { + return Projected.wrap(U3853.createUser()).include("(id, address(city))"); + }); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java index b36078b95c..7406f01b44 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java +++ b/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java @@ -12,6 +12,409 @@ import io.swagger.v3.oas.models.SpecVersion; public class Issue3853 { + + @OpenAPITest(value = App3853Script.class) + public void shouldDetectProjectAnnotationScript(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.0.1 + info: + title: 3853Script API + description: 3853Script API description + version: "1.0" + paths: + /3853/{id}: + get: + operationId: get3853Id + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-variable: + get: + operationId: get3853ListVariable + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-variable-var: + get: + operationId: get3853ListVariableVar + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-call: + get: + operationId: get3853ListCall + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-call-variable: + get: + operationId: get3853ListCallVariable + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list: + get: + operationId: get3853List + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/address: + get: + operationId: get3853Address + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_ff52rt" + /3853/address/partial: + get: + operationId: get3853AddressPartial + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_xsv0o" + components: + schemas: + U3853: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + address: + $ref: "#/components/schemas/A3853" + roles: + type: array + description: Retrieves the list of roles associated with the user. + items: + $ref: "#/components/schemas/R3853" + meta: + type: object + additionalProperties: + type: string + description: Retrieves the metadata associated with the user. + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + L3853: + type: object + properties: + lat: + type: number + format: double + lon: + type: number + format: double + R3853: + type: object + properties: + name: + type: string + level: + type: integer + format: int32 + A3853: + type: object + properties: + city: + type: string + loc: + $ref: "#/components/schemas/L3853" + U3853_heab3f: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + U3853_ff52rt: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853" + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + A3853_1tgff: + type: object + properties: + city: + type: string + U3853_xsv0o: + type: object + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853_1tgff" + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + """); + } + + @OpenAPITest(value = App3853Script.class, version = SpecVersion.V31) + public void shouldDetectProjectAnnotationScriptV31(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.1.0 + info: + title: 3853Script API + description: 3853Script API description + version: "1.0" + paths: + /3853/{id}: + get: + operationId: get3853Id + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-variable: + get: + operationId: get3853ListVariable + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-variable-var: + get: + operationId: get3853ListVariableVar + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-call: + get: + operationId: get3853ListCall + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list-call-variable: + get: + operationId: get3853ListCallVariable + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/list: + get: + operationId: get3853List + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/U3853_heab3f" + /3853/address: + get: + operationId: get3853Address + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_ff52rt" + /3853/address/partial: + get: + operationId: get3853AddressPartial + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/U3853_xsv0o" + components: + schemas: + U3853: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + address: + $ref: "#/components/schemas/A3853" + description: Retrieves the address associated with the user. + roles: + type: array + description: Retrieves the list of roles associated with the user. + items: + $ref: "#/components/schemas/R3853" + meta: + type: object + additionalProperties: + type: string + description: Retrieves the metadata associated with the user. + L3853: + type: object + properties: + lat: + type: number + format: double + lon: + type: number + format: double + R3853: + type: object + properties: + name: + type: string + level: + type: integer + format: int32 + A3853: + type: object + properties: + city: + type: string + loc: + $ref: "#/components/schemas/L3853" + U3853_heab3f: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + name: + type: string + description: Retrieves the name of the user. + U3853_ff52rt: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853" + description: Retrieves the address associated with the user. + A3853_1tgff: + type: object + properties: + city: + type: string + U3853_xsv0o: + type: object + description: "Represents a user entity identified by an ID and name, with associated\\ + \\ address details, roles, and metadata. This class is immutable, ensuring\\ + \\ the integrity of its fields." + properties: + id: + type: string + description: Retrieves the unique identifier for the user. + address: + $ref: "#/components/schemas/A3853_1tgff" + """); + } + @OpenAPITest(value = App3853.class) public void shouldDetectProjectAnnotation(OpenAPIResult result) { assertThat(result.toYaml()) From 0caf5bea6b470891f949cfe65f1f8a530d2c2087 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Feb 2026 14:24:00 -0300 Subject: [PATCH 25/31] revert context.adoc --- docs/asciidoc/context.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/asciidoc/context.adoc b/docs/asciidoc/context.adoc index abf769983c..47f5921248 100644 --- a/docs/asciidoc/context.adoc +++ b/docs/asciidoc/context.adoc @@ -406,7 +406,7 @@ curl -d "id=root&pass=pwd" -X POST http://localhost:8080/user <2> Form as `multi-value map` => `{id=root, pass=[pwd]}` <3> Form variable `id` => `root` <4> Form variable `pass` => `pwd` -<5> Form as `U3853` object => `User(id=root, pass=pwd)` +<5> Form as `User` object => `User(id=root, pass=pwd)` ==== Multipart @@ -482,7 +482,7 @@ curl -F id=root -F pass=root -F pic=@/path/to/local/file/profile.png http://loca <3> Form variable `id` => `root` <4> Form variable `pass` => `pwd` <5> javadoc:FileUpload[] variable `pic` -<6> Form as `U3853` object => `User(id=root, pass=pwd, pic=profile.png)` +<6> Form as `User` object => `User(id=root, pass=pwd, pic=profile.png)` [NOTE] .File Upload From 010b0c2194580b8d12900cc05e6b04021c05b399 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Feb 2026 15:06:29 -0300 Subject: [PATCH 26/31] FEATURE: Unified JSON Projection API - add documentation - fix #3853 --- docs/asciidoc/modules/modules.adoc | 2 +- docs/asciidoc/mvc-api.adoc | 53 ++++++++++++++++++++++++ docs/asciidoc/responses.adoc | 66 ++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/docs/asciidoc/modules/modules.adoc b/docs/asciidoc/modules/modules.adoc index 43bd4588df..8029f4b8be 100644 --- a/docs/asciidoc/modules/modules.adoc +++ b/docs/asciidoc/modules/modules.adoc @@ -40,11 +40,11 @@ Modules are distributed as separate dependencies. Below is the catalog of offici * link:{uiVersion}/modules/vertx[Vertx]: Vertx module for Jooby. ==== JSON + * link:{uiVersion}/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby. * link:{uiVersion}/modules/gson[Gson]: Gson module for Jooby. * link:{uiVersion}/modules/jackson2[Jackson2]: Jackson2 module for Jooby. * link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby. * link:{uiVersion}/modules/yasson[JSON-B]: JSON-B module for Jooby. - * link:{uiVersion}/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby. ==== OpenAPI * link:{uiVersion}/modules/openapi[OpenAPI]: OpenAPI supports. diff --git a/docs/asciidoc/mvc-api.adoc b/docs/asciidoc/mvc-api.adoc index fcda32b4fa..9e719311c5 100644 --- a/docs/asciidoc/mvc-api.adoc +++ b/docs/asciidoc/mvc-api.adoc @@ -577,6 +577,59 @@ If a request is made to `/bar?foo=baz`, the result will be `foo is: baz` because ==== Responses +===== Projections + +The MVC module provides first-class support for Projections via annotations. This allows you to define the response view declaratively, keeping your controller logic clean and focused on data retrieval. + +====== Usage + +There are two ways to define a projection in an MVC controller. + +You can annotate your method with `@Project` and provide the selection DSL: + +.Via @Project Annotation +[source,java] +---- +@GET +@Project("(id, name)") +public List listUsers() { + return service.findUsers(); +} +---- + +Alternatively, you can define the projection directly within the HTTP method annotation (e.g., `@GET`, `@POST`) using the `projection` attribute: + +.Via HTTP Method Attribute +[source,java] +---- +@GET(value = "/users", projection = "(id, name, email)") +public List listUsers() { + return service.findUsers(); +} +---- + +====== Automatic Wrapping + +The Jooby Annotation Processor automatically handles the conversion of your method's return type. You are **not forced** to return a `Projected` instance; you can simply return your POJO or Collection, and Jooby will wrap it for you at compile-time. + +However, if you need manual control (for example, to dynamically toggle validation), you can still return a `Projected` instance explicitly: + +[source,java] +---- +@GET +public Projected getUser(String id) { + User user = service.findById(id); + return Projected.wrap(user) + .failOnMissingProperty(true) + .include("(id, status)"); +} +---- + +[NOTE] +==== +For more details on the Selection DSL syntax and available JSON engines, please refer to the <>. +==== + ===== Status Code The default HTTP status code returned by an MVC route is `200 OK`, except for `void` methods annotated with `@DELETE`, which automatically return `204 No Content`. diff --git a/docs/asciidoc/responses.adoc b/docs/asciidoc/responses.adoc index 160e2c3e9d..baa4258c51 100644 --- a/docs/asciidoc/responses.adoc +++ b/docs/asciidoc/responses.adoc @@ -37,6 +37,72 @@ Raw responses are **not** processed by a < { + User user = repository.findById(ctx.path("id").value()); + + return Projected.wrap(user) + .include("(id, name, email)"); +}); +---- + +===== Comparison with @JsonView + +If you have used Jackson's `@JsonView`, you will find Projections far more capable: + +* **Dynamic**: Unlike `@JsonView`, which requires static class markers defined at compile-time, Projections are defined at runtime. +* **Ad-hoc**: You can create any combination of fields on the fly without adding new Java interfaces or classes. +* **Deep Nesting**: Projections easily handle deeply nested object graphs, whereas `@JsonView` can become difficult to manage with complex relationships. + +===== Projection DSL + +The `include` method accepts a string using a simple, nested syntax: + +* **Field Selection**: `(id, name)` returns only those two fields. +* **Nested Selection**: `(id, address(city, country))` selects the `id` and specific fields from the nested `address` object. +* **Wildcards**: `(id, address(*))` selects the `id` and all available fields within the `address` object. +* **Deep Nesting**: `(id, orders(id, items(name, price)))` allows for recursion into the object graph. + +===== Validation + +By default, the projection engine **does not validate** that requested fields exist on the target class (`failOnMissingProperty` is `false`). This allows for maximum flexibility, especially when working with polymorphic types or dynamic data where certain fields may only exist on specific subclasses. + +If you prefer strict enforcement to prevent API consumers from requesting non-existent fields, you can enable validation: + +[source,java] +---- +return Projected.wrap(data) + .failOnMissingProperty(true) + .include("(id, name, strictFieldOnly)"); +---- + +===== More Information + +Support for Projections extends beyond core scripting to include high-level annotations and documentation generation. + +* **MVC Support**: Projections can be applied to controller methods using the `@Project` annotation. See the <> for details. +* **OpenAPI Support**: Jooby automatically generates pruned schemas for your Swagger documentation. See the link:modules/openapi[OpenAPI documentation] for details. + +[NOTE] +==== +**Implementation Note:** +The `Projection` core API defines the structure and the DSL. The actual runtime filtering is performed by your chosen JSON module: + +1. link:modules/avaje-jsonb[Avaje Jsonb] +2. link:modules/jackson2[Jackson 2]/link:modules/jackson3[Jackson 3] +==== + ==== Streaming / Chunked The Streaming/Chunked API is available via: From 669c5a5401eb213bea56324772c3e67a06495799 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Feb 2026 15:30:16 -0300 Subject: [PATCH 27/31] openapi: purge/clear unused schemas from output fix #3862 --- .../jooby/internal/openapi/SchemaPurger.java | 230 ++++++++++++++++++ .../io/jooby/openapi/OpenAPIGenerator.java | 4 +- .../io/jooby/openapi/OpenAPIYamlTest.java | 32 +-- .../java/issues/i3729/api/ApiDocTest.java | 15 -- .../src/test/java/issues/i3853/Issue3853.java | 130 ---------- 5 files changed, 235 insertions(+), 176 deletions(-) create mode 100644 modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/SchemaPurger.java diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/SchemaPurger.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/SchemaPurger.java new file mode 100644 index 0000000000..73b0663f44 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/SchemaPurger.java @@ -0,0 +1,230 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi; + +import java.util.*; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; + +public class SchemaPurger { + + public static void purgeUnused(OpenAPI openAPI) { + if (openAPI == null + || openAPI.getComponents() == null + || openAPI.getComponents().getSchemas() == null) { + return; + } + + Set visitedSchemas = new HashSet<>(); + Queue queue = new LinkedList<>(); + + // 1. Gather Roots (Using your visitor/parser) + // Scan Paths, Parameters, Responses, and RequestBodies for $refs pointing to schemas. + Set rootSchemaNames = extractRootSchemaNames(openAPI); + + for (String schemaName : rootSchemaNames) { + if (visitedSchemas.add(schemaName)) { + queue.add(schemaName); + } + } + + Map allSchemas = openAPI.getComponents().getSchemas(); + + // 2. Traverse Graph (BFS) + while (!queue.isEmpty()) { + String currentName = queue.poll(); + Schema currentSchema = allSchemas.get(currentName); + + if (currentSchema == null) continue; + + // Scan this schema for nested $refs (properties, items, allOf, anyOf, oneOf) + Set nestedSchemaNames = extractSchemaNamesFromSchema(currentSchema); + + for (String nestedName : nestedSchemaNames) { + // CIRCULAR REFERENCE CHECK: + // visitedSchemas.add() returns false if the element is already present. + // If it's already visited, we ignore it, breaking the cycle. + if (visitedSchemas.add(nestedName)) { + queue.add(nestedName); + } + } + } + + // 3. Purge Unused + // retainAll efficiently drops any key from the components map that isn't in our visited set. + allSchemas.keySet().retainAll(visitedSchemas); + } + + // --- Helper Methods (To be integrated with your OpenAPI visitor) --- + + private static Set extractRootSchemaNames(OpenAPI openAPI) { + Set roots = new HashSet<>(); + + // 1. Scan Paths for schemas used in operations + if (openAPI.getPaths() != null) { + openAPI + .getPaths() + .values() + .forEach( + pathItem -> { + + // Check path-level parameters + if (pathItem.getParameters() != null) { + pathItem + .getParameters() + .forEach( + param -> roots.addAll(extractSchemaNamesFromSchema(param.getSchema()))); + } + + if (pathItem.readOperations() != null) { + pathItem + .readOperations() + .forEach( + operation -> { + + // Check operation parameters (Query, Path, Header, etc.) + if (operation.getParameters() != null) { + operation + .getParameters() + .forEach( + param -> + roots.addAll( + extractSchemaNamesFromSchema(param.getSchema()))); + } + + // Check Request Bodies + if (operation.getRequestBody() != null + && operation.getRequestBody().getContent() != null) { + operation + .getRequestBody() + .getContent() + .values() + .forEach( + mediaType -> + roots.addAll( + extractSchemaNamesFromSchema(mediaType.getSchema()))); + } + + // Check Responses + if (operation.getResponses() != null) { + operation + .getResponses() + .values() + .forEach( + response -> { + if (response.getContent() != null) { + response + .getContent() + .values() + .forEach( + mediaType -> + roots.addAll( + extractSchemaNamesFromSchema( + mediaType.getSchema()))); + } + }); + } + }); + } + }); + } + + // 2. Scan Components for shared non-schema objects that reference schemas + if (openAPI.getComponents() != null) { + + // Shared Parameters + if (openAPI.getComponents().getParameters() != null) { + openAPI + .getComponents() + .getParameters() + .values() + .forEach(param -> roots.addAll(extractSchemaNamesFromSchema(param.getSchema()))); + } + + // Shared Responses + if (openAPI.getComponents().getResponses() != null) { + openAPI + .getComponents() + .getResponses() + .values() + .forEach( + response -> { + if (response.getContent() != null) { + response + .getContent() + .values() + .forEach( + mediaType -> + roots.addAll(extractSchemaNamesFromSchema(mediaType.getSchema()))); + } + }); + } + + // Shared RequestBodies + if (openAPI.getComponents().getRequestBodies() != null) { + openAPI + .getComponents() + .getRequestBodies() + .values() + .forEach( + requestBody -> { + if (requestBody.getContent() != null) { + requestBody + .getContent() + .values() + .forEach( + mediaType -> + roots.addAll(extractSchemaNamesFromSchema(mediaType.getSchema()))); + } + }); + } + } + + return roots; + } + + private static Set extractSchemaNamesFromSchema(Schema schema) { + Set refs = new HashSet<>(); + + // 1. Check direct ref + if (schema.get$ref() != null) { + refs.add(extractName(schema.get$ref())); + } + + // 2. Check properties + if (schema.getProperties() != null) { + schema + .getProperties() + .values() + .forEach(prop -> refs.addAll(extractSchemaNamesFromSchema(prop))); + } + + // 3. Check arrays (items) + if (schema.getItems() != null) { + refs.addAll(extractSchemaNamesFromSchema(schema.getItems())); + } + + // 4. Check compositions + if (schema.getAllOf() != null) + schema.getAllOf().forEach(s -> refs.addAll(extractSchemaNamesFromSchema(s))); + if (schema.getAnyOf() != null) + schema.getAnyOf().forEach(s -> refs.addAll(extractSchemaNamesFromSchema(s))); + if (schema.getOneOf() != null) + schema.getOneOf().forEach(s -> refs.addAll(extractSchemaNamesFromSchema(s))); + + // 5. Check additionalProperties (Maps) + if (schema.getAdditionalProperties() instanceof Schema) { + refs.addAll(extractSchemaNamesFromSchema((Schema) schema.getAdditionalProperties())); + } + + return refs; + } + + private static String extractName(String ref) { + return ref != null ? ref.substring(ref.lastIndexOf('/') + 1) : null; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java index 67718cfdfa..8f9046e07f 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java @@ -326,7 +326,9 @@ public OpenAPIGenerator() {} return openapi; } - private void finish(OpenAPIExt openapi) {} + private void finish(OpenAPIExt openapi) { + SchemaPurger.purgeUnused(openapi); + } ObjectMapper yamlMapper() { return specVersion == SpecVersion.V30 ? Yaml.mapper() : Yaml31.mapper(); diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java index 223566efda..83463b0c9f 100644 --- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java @@ -161,21 +161,7 @@ public void shouldGenerateMinApp(OpenAPIResult result) { + " type: integer\n" + " format: int64\n" + " name:\n" - + " type: string\n" - + " PetQuery:\n" - + " type: object\n" - + " properties:\n" - + " id:\n" - + " type: integer\n" - + " format: int64\n" - + " name:\n" - + " type: string\n" - + " start:\n" - + " type: integer\n" - + " format: int32\n" - + " max:\n" - + " type: integer\n" - + " format: int32\n", + + " type: string\n", result.toYaml()); } @@ -326,21 +312,7 @@ public void shouldGenerateKtMinApp(OpenAPIResult result) { + " type: integer\n" + " format: int64\n" + " name:\n" - + " type: string\n" - + " PetQuery:\n" - + " type: object\n" - + " properties:\n" - + " id:\n" - + " type: integer\n" - + " format: int64\n" - + " name:\n" - + " type: string\n" - + " start:\n" - + " type: integer\n" - + " format: int32\n" - + " max:\n" - + " type: integer\n" - + " format: int32\n", + + " type: string\n", result.toYaml()); } diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java index d8bc609cc7..f5bbfbb0db 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java @@ -566,21 +566,6 @@ private void checkResult(OpenAPIResult result) { description: Published books. items: $ref: "#/components/schemas/Book" - BookQuery: - type: object - properties: - title: - type: string - description: Book's title. - author: - type: string - description: Book's author. Optional. - isbn: - type: array - description: Book's isbn. Optional. - items: - type: string - description: Query books by complex filters. Address: type: object properties: diff --git a/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java b/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java index 7406f01b44..b233d8d3e3 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java +++ b/modules/jooby-openapi/src/test/java/issues/i3853/Issue3853.java @@ -123,30 +123,6 @@ public void shouldDetectProjectAnnotationScript(OpenAPIResult result) { $ref: "#/components/schemas/U3853_xsv0o" components: schemas: - U3853: - type: object - properties: - id: - type: string - description: Retrieves the unique identifier for the user. - name: - type: string - description: Retrieves the name of the user. - address: - $ref: "#/components/schemas/A3853" - roles: - type: array - description: Retrieves the list of roles associated with the user. - items: - $ref: "#/components/schemas/R3853" - meta: - type: object - additionalProperties: - type: string - description: Retrieves the metadata associated with the user. - description: "Represents a user entity identified by an ID and name, with associated\\ - \\ address details, roles, and metadata. This class is immutable, ensuring\\ - \\ the integrity of its fields." L3853: type: object properties: @@ -156,14 +132,6 @@ public void shouldDetectProjectAnnotationScript(OpenAPIResult result) { lon: type: number format: double - R3853: - type: object - properties: - name: - type: string - level: - type: integer - format: int32 A3853: type: object properties: @@ -323,31 +291,6 @@ public void shouldDetectProjectAnnotationScriptV31(OpenAPIResult result) { $ref: "#/components/schemas/U3853_xsv0o" components: schemas: - U3853: - type: object - description: "Represents a user entity identified by an ID and name, with associated\\ - \\ address details, roles, and metadata. This class is immutable, ensuring\\ - \\ the integrity of its fields." - properties: - id: - type: string - description: Retrieves the unique identifier for the user. - name: - type: string - description: Retrieves the name of the user. - address: - $ref: "#/components/schemas/A3853" - description: Retrieves the address associated with the user. - roles: - type: array - description: Retrieves the list of roles associated with the user. - items: - $ref: "#/components/schemas/R3853" - meta: - type: object - additionalProperties: - type: string - description: Retrieves the metadata associated with the user. L3853: type: object properties: @@ -357,14 +300,6 @@ public void shouldDetectProjectAnnotationScriptV31(OpenAPIResult result) { lon: type: number format: double - R3853: - type: object - properties: - name: - type: string - level: - type: integer - format: int32 A3853: type: object properties: @@ -498,30 +433,6 @@ public void shouldDetectProjectAnnotation(OpenAPIResult result) { $ref: "#/components/schemas/U3853_xsv0o" components: schemas: - U3853: - type: object - properties: - id: - type: string - description: Retrieves the unique identifier for the user. - name: - type: string - description: Retrieves the name of the user. - address: - $ref: "#/components/schemas/A3853" - roles: - type: array - description: Retrieves the list of roles associated with the user. - items: - $ref: "#/components/schemas/R3853" - meta: - type: object - additionalProperties: - type: string - description: Retrieves the metadata associated with the user. - description: "Represents a user entity identified by an ID and name, with associated\\ - \\ address details, roles, and metadata. This class is immutable, ensuring\\ - \\ the integrity of its fields." L3853: type: object properties: @@ -531,14 +442,6 @@ public void shouldDetectProjectAnnotation(OpenAPIResult result) { lon: type: number format: double - R3853: - type: object - properties: - name: - type: string - level: - type: integer - format: int32 A3853: type: object properties: @@ -680,31 +583,6 @@ public void shouldDetectProjectAnnotationV31(OpenAPIResult result) { $ref: "#/components/schemas/U3853_xsv0o" components: schemas: - U3853: - type: object - description: "Represents a user entity identified by an ID and name, with associated\\ - \\ address details, roles, and metadata. This class is immutable, ensuring\\ - \\ the integrity of its fields." - properties: - id: - type: string - description: Retrieves the unique identifier for the user. - name: - type: string - description: Retrieves the name of the user. - address: - $ref: "#/components/schemas/A3853" - description: Retrieves the address associated with the user. - roles: - type: array - description: Retrieves the list of roles associated with the user. - items: - $ref: "#/components/schemas/R3853" - meta: - type: object - additionalProperties: - type: string - description: Retrieves the metadata associated with the user. L3853: type: object properties: @@ -714,14 +592,6 @@ public void shouldDetectProjectAnnotationV31(OpenAPIResult result) { lon: type: number format: double - R3853: - type: object - properties: - name: - type: string - level: - type: integer - format: int32 A3853: type: object properties: From 016e37d75e566ee1a123b57191a6b0ec01342264 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Feb 2026 15:44:08 -0300 Subject: [PATCH 28/31] doc: adjust font --- docs/js/styles/theme.css | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/js/styles/theme.css b/docs/js/styles/theme.css index ea246c1226..0b781eb15e 100644 --- a/docs/js/styles/theme.css +++ b/docs/js/styles/theme.css @@ -31,6 +31,37 @@ /* Modern Developer Font Stack */ --font-code: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1. The Modern Developer System Stack + This bypasses old default fonts and uses the highly-optimized UI font of whatever OS the user is on (San Francisco on Mac, Segoe UI on Windows, Roboto on Android) */ + --font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +body { + font-family: var(--font-main); + font-size: 16px; /* Ensure a solid base size */ + line-height: 1.65; /* Gives the lines a bit more breathing room */ + + /* 2. Dark Mode Legibility Magic + These three properties stop the text from looking thin/jagged on dark backgrounds */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + + /* 3. A slightly crisper text color. + Pure white (#FFF) is too harsh, but a bright slate grey pops beautifully */ + color: #e2e8f0; +} + +/* Ensure standard paragraphs use the spacing properly */ +p { + margin-top: 0; + margin-bottom: 1.25rem; +} + +/* Optional polish for your bold text to make it stand out better against the new crisp text */ +strong, b { + font-weight: 600; + color: #f8fafc; /* Slightly brighter than normal text */ } pre.highlightjs, .highlightjs code { From 4674b52a9534a2afa497a1235b64e57f0e130842 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Feb 2026 20:51:21 -0300 Subject: [PATCH 29/31] - remove method-ref from Projection, just keep string projection - ref #3853 --- jooby/src/main/java/io/jooby/Projected.java | 12 --- jooby/src/main/java/io/jooby/Projection.java | 75 ------------------- .../test/java/io/jooby/ProjectionTest.java | 8 -- .../test/java/io/jooby/i3853/Issue3853.java | 16 ---- 4 files changed, 111 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Projected.java b/jooby/src/main/java/io/jooby/Projected.java index a230bdcd74..eaeefb3722 100644 --- a/jooby/src/main/java/io/jooby/Projected.java +++ b/jooby/src/main/java/io/jooby/Projected.java @@ -6,7 +6,6 @@ package io.jooby; import java.util.*; -import java.util.function.Consumer; /** * A wrapper for a value and its associated {@link Projection}. @@ -56,17 +55,6 @@ public Projected include(String... paths) { return this; } - @SafeVarargs - public final Projected include(Projection.Property... props) { - projection.include(props); - return this; - } - - public Projected include(Projection.Property prop, Consumer> child) { - projection.include(prop, child); - return this; - } - @Override public String toString() { return projection.toString(); diff --git a/jooby/src/main/java/io/jooby/Projection.java b/jooby/src/main/java/io/jooby/Projection.java index 8f913136ed..c3ece608c6 100644 --- a/jooby/src/main/java/io/jooby/Projection.java +++ b/jooby/src/main/java/io/jooby/Projection.java @@ -5,15 +5,12 @@ */ package io.jooby; -import java.io.Serializable; -import java.lang.invoke.SerializedLambda; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; import io.jooby.value.ValueFactory; @@ -90,23 +87,6 @@ */ public class Projection { - /** - * Functional interface for capturing method references. - * - * @param The type containing the property. - * @param The return type of the property. - */ - @FunctionalInterface - public interface Property extends Serializable { - /** - * Captures the property method reference. - * - * @param instance The instance to apply the method to. - * @return The property value. - */ - R apply(T instance); - } - private static final Map, String> PROP_CACHE = new ConcurrentHashMap<>(); private final Class type; @@ -152,44 +132,6 @@ public Projection include(String... paths) { return this; } - /** - * Includes fields via type-safe method references. - * - * @param props Method references. - * @return This projection instance. - */ - @SafeVarargs - public final Projection include(Property... props) { - for (Property prop : props) { - String name = getFieldName(prop); - children.computeIfAbsent( - name, k -> new Projection<>(resolveFieldType(this.type, name), false, validate)); - } - rebuild(); - return this; - } - - /** - * Includes a nested field and configures its sub-projection using a lambda. This provides full - * type-safety for nested objects. - * - * @param The type of the nested field. - * @param prop The method reference to the nested field. - * @param childSpec A consumer that configures the nested projection. - * @return This projection instance. - */ - public Projection include(Property prop, Consumer> childSpec) { - String name = getFieldName(prop); - Class childType = (Class) resolveFieldType(this.type, name); - Projection child = - (Projection) - children.computeIfAbsent(name, k -> new Projection<>(childType, false, validate)); - childSpec.accept(child); - child.rebuild(); - rebuild(); - return this; - } - public Map> getChildren() { return Collections.unmodifiableMap(children); } @@ -552,23 +494,6 @@ private Class resolveFieldType(Class currentType, String fieldName) { return rawType; } - private static String getFieldName(Property property) { - return PROP_CACHE.computeIfAbsent( - property.getClass(), - clz -> { - try { - Method m = clz.getDeclaredMethod("writeReplace"); - m.setAccessible(true); - SerializedLambda l = (SerializedLambda) m.invoke(property); - String n = l.getImplMethodName(); - int s = n.startsWith("get") ? 3 : (n.startsWith("is") ? 2 : 0); - return Character.toLowerCase(n.charAt(s)) + n.substring(s + 1); - } catch (Exception x) { - throw new IllegalArgumentException("Could not resolve field from method reference.", x); - } - }); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/jooby/src/test/java/io/jooby/ProjectionTest.java b/jooby/src/test/java/io/jooby/ProjectionTest.java index 2bc16d310b..ab7437953e 100644 --- a/jooby/src/test/java/io/jooby/ProjectionTest.java +++ b/jooby/src/test/java/io/jooby/ProjectionTest.java @@ -131,14 +131,6 @@ public void testMixedNotationRecursive() { assertEquals("(address(loc(lat,lon)),roles(name))", p.toView()); } - @Test - public void testTypeSafeInclude() { - // Type-safe references also follow the defined order - Projection p = Projection.of(User.class).include(User::getName, User::getId); - assertEquals("(name,id)", p.toView()); - assertEquals("User(name,id)", p.toString()); - } - @Test public void testCollectionGenericUnwrapping() { Projection p = Projection.of(User.class).include("roles.name"); diff --git a/tests/src/test/java/io/jooby/i3853/Issue3853.java b/tests/src/test/java/io/jooby/i3853/Issue3853.java index 92d434a559..62e663d8a0 100644 --- a/tests/src/test/java/io/jooby/i3853/Issue3853.java +++ b/tests/src/test/java/io/jooby/i3853/Issue3853.java @@ -102,13 +102,6 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { ctx -> { return Projected.wrap(createUser()).include("(id, name, address(loc(lat)))"); }); - app.get( - "/stub/address-stub-ref", - ctx -> { - return Projected.wrap(createUser()) - .include(U3853::getId, U3853::getName) - .include(U3853::getAddress, addr -> addr.include(A3853::getCity)); - }); }) .ready( http -> { @@ -189,15 +182,6 @@ public void shouldProjectData(ServerTestRunner runner, Extension extension) { {"id":"cobb-001","name":"Dom Cobb","address":{"city":"Snow Fortress (Level 3)"}} """); }); - http.get( - "/stub/address-stub-ref", - rsp -> { - assertThat(rsp.body().string()) - .isEqualToIgnoringNewLines( - """ - {"id":"cobb-001","name":"Dom Cobb","address":{"city":"Snow Fortress (Level 3)"}} - """); - }); http.get( "/stub/address-loc-lat", rsp -> { From 3d201fc5f1b2b7ddc32bf82e66355db22564b441 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 00:08:52 +0000 Subject: [PATCH 30/31] build(deps): bump the dependencies group with 8 updates Bumps the dependencies group with 8 updates: | Package | From | To | | --- | --- | --- | | [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) | `2.21.0` | `2.21.1` | | [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) | `3.0.4` | `3.1.0` | | [com.typesafe:config](https://github.com/lightbend/config) | `1.4.5` | `1.4.6` | | [org.mockito:mockito-core](https://github.com/mockito/mockito) | `5.21.0` | `5.22.0` | | [org.mockito:mockito-junit-jupiter](https://github.com/mockito/mockito) | `5.21.0` | `5.22.0` | | [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) | `13.2.0` | `13.3.0` | | [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) | `1.18.5` | `1.18.7` | | software.amazon.awssdk:bom | `2.41.34` | `2.42.4` | Updates `com.fasterxml.jackson:jackson-bom` from 2.21.0 to 2.21.1 - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.21.0...jackson-bom-2.21.1) Updates `tools.jackson:jackson-bom` from 3.0.4 to 3.1.0 - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.0.4...jackson-bom-3.1.0) Updates `com.typesafe:config` from 1.4.5 to 1.4.6 - [Release notes](https://github.com/lightbend/config/releases) - [Changelog](https://github.com/lightbend/config/blob/main/NEWS.md) - [Commits](https://github.com/lightbend/config/compare/v1.4.5...v1.4.6) Updates `org.mockito:mockito-core` from 5.21.0 to 5.22.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.21.0...v5.22.0) Updates `org.mockito:mockito-junit-jupiter` from 5.21.0 to 5.22.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.21.0...v5.22.0) Updates `org.mockito:mockito-junit-jupiter` from 5.21.0 to 5.22.0 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.21.0...v5.22.0) Updates `com.puppycrawl.tools:checkstyle` from 13.2.0 to 13.3.0 - [Release notes](https://github.com/checkstyle/checkstyle/releases) - [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-13.2.0...checkstyle-13.3.0) Updates `net.bytebuddy:byte-buddy` from 1.18.5 to 1.18.7 - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.18.5...byte-buddy-1.18.7) Updates `software.amazon.awssdk:bom` from 2.41.34 to 2.42.4 --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: tools.jackson:jackson-bom dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: com.typesafe:config dependency-version: 1.4.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: org.mockito:mockito-core dependency-version: 5.22.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.mockito:mockito-junit-jupiter dependency-version: 5.22.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.mockito:mockito-junit-jupiter dependency-version: 5.22.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: com.puppycrawl.tools:checkstyle dependency-version: 13.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.42.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 4 ++-- pom.xml | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 89f3b400c3..8d8f462472 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.41.34 + 2.42.4 diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 13f2837b88..33002c8d5d 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -53,7 +53,7 @@ com.puppycrawl.tools checkstyle - 13.2.0 + 13.3.0 @@ -156,7 +156,7 @@ net.bytebuddy byte-buddy - 1.18.5 + 1.18.7 test diff --git a/pom.xml b/pom.xml index 8214a7539b..2fce3125bb 100644 --- a/pom.xml +++ b/pom.xml @@ -61,8 +61,8 @@ 4.5.0 1.3.7 4.1.1 - 2.21.0 - 3.0.4 + 2.21.1 + 3.1.0 2.13.2 3.0.1 3.0.4 @@ -83,7 +83,7 @@ 4.2.0 3.2.3 - 1.4.5 + 1.4.6 7.0.0 @@ -153,7 +153,7 @@ 6.0.3 6.0.0 3.27.7 - 5.21.0 + 5.22.0 ${user.home}${file.separator}.m2${file.separator}repository org${file.separator}mockito${file.separator}mockito-core${file.separator}${mockito.version}${file.separator}mockito-core-${mockito.version}.jar -javaagent:${maven.m2.repo}${file.separator}${mockito.agent} From c121f3ff9d4ff1207772b43158d0ce6f20fdcaa9 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 2 Mar 2026 13:38:43 -0300 Subject: [PATCH 31/31] v4.0.16 --- jooby/pom.xml | 2 +- modules/jooby-apt/pom.xml | 2 +- modules/jooby-avaje-inject/pom.xml | 2 +- modules/jooby-avaje-jsonb/pom.xml | 2 +- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v1/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-bom/pom.xml | 4 ++-- modules/jooby-caffeine/pom.xml | 2 +- modules/jooby-camel/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-commons-email/pom.xml | 2 +- modules/jooby-conscrypt/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-distribution/pom.xml | 2 +- modules/jooby-ebean/pom.xml | 2 +- modules/jooby-flyway/pom.xml | 2 +- modules/jooby-freemarker/pom.xml | 2 +- modules/jooby-gradle-setup/pom.xml | 2 +- modules/jooby-graphiql/pom.xml | 2 +- modules/jooby-graphql/pom.xml | 2 +- modules/jooby-gson/pom.xml | 2 +- modules/jooby-guice/pom.xml | 2 +- modules/jooby-handlebars/pom.xml | 2 +- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-hibernate/pom.xml | 2 +- modules/jooby-hikari/pom.xml | 2 +- modules/jooby-jackson/pom.xml | 2 +- modules/jooby-jackson3/pom.xml | 2 +- modules/jooby-jasypt/pom.xml | 2 +- modules/jooby-jdbi/pom.xml | 2 +- modules/jooby-jetty/pom.xml | 2 +- modules/jooby-jstachio/pom.xml | 2 +- modules/jooby-jte/pom.xml | 2 +- modules/jooby-jwt/pom.xml | 2 +- modules/jooby-kafka/pom.xml | 2 +- modules/jooby-kotlin/pom.xml | 2 +- modules/jooby-log4j/pom.xml | 2 +- modules/jooby-logback/pom.xml | 2 +- modules/jooby-maven-plugin/pom.xml | 2 +- modules/jooby-metrics/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-netty/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-pac4j/pom.xml | 2 +- modules/jooby-pebble/pom.xml | 2 +- modules/jooby-quartz/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-redis/pom.xml | 2 +- modules/jooby-redoc/pom.xml | 2 +- modules/jooby-rocker/pom.xml | 2 +- modules/jooby-run/pom.xml | 2 +- modules/jooby-rxjava3/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- modules/jooby-swagger-ui/pom.xml | 2 +- modules/jooby-test/pom.xml | 2 +- modules/jooby-thymeleaf/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- modules/jooby-vertx-mysql-client/pom.xml | 2 +- modules/jooby-vertx-pg-client/pom.xml | 2 +- modules/jooby-vertx-sql-client/pom.xml | 2 +- modules/jooby-vertx/pom.xml | 2 +- modules/jooby-whoops/pom.xml | 2 +- modules/jooby-yasson/pom.xml | 2 +- modules/pom.xml | 2 +- pom.xml | 4 ++-- tests/pom.xml | 2 +- 67 files changed, 69 insertions(+), 69 deletions(-) diff --git a/jooby/pom.xml b/jooby/pom.xml index 659b29a13f..d6b781528e 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.16-SNAPSHOT + 4.0.16 jooby jooby diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index 454298979c..2ee8a6add5 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-apt jooby-apt diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml index c0f2e8243c..4180caa86e 100644 --- a/modules/jooby-avaje-inject/pom.xml +++ b/modules/jooby-avaje-inject/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-avaje-inject jooby-avaje-inject diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index 1c0fe29d63..409ca6cf0c 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-avaje-jsonb jooby-avaje-jsonb diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index 16f68c8702..c30788ec2b 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-avaje-validator jooby-avaje-validator diff --git a/modules/jooby-awssdk-v1/pom.xml b/modules/jooby-awssdk-v1/pom.xml index 2fa34972ad..82d2ff98ea 100644 --- a/modules/jooby-awssdk-v1/pom.xml +++ b/modules/jooby-awssdk-v1/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-awssdk-v1 jooby-awssdk-v1 diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 8d8f462472..8b4fb9ff15 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-awssdk-v2 jooby-awssdk-v2 diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index e7bf79f39d..3cba8f7fc9 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -7,14 +7,14 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 io.jooby jooby-bom jooby-bom pom - 4.0.16-SNAPSHOT + 4.0.16 Jooby (Bill of Materials) https://jooby.io diff --git a/modules/jooby-caffeine/pom.xml b/modules/jooby-caffeine/pom.xml index 6e7423414a..a6c318be9b 100644 --- a/modules/jooby-caffeine/pom.xml +++ b/modules/jooby-caffeine/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-caffeine jooby-caffeine diff --git a/modules/jooby-camel/pom.xml b/modules/jooby-camel/pom.xml index d38094d3e5..b05c86f2c2 100644 --- a/modules/jooby-camel/pom.xml +++ b/modules/jooby-camel/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-camel jooby-camel diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 5f2a666d6b..4e931f8a9b 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-cli jooby-cli diff --git a/modules/jooby-commons-email/pom.xml b/modules/jooby-commons-email/pom.xml index 26720bd407..b648eba77c 100644 --- a/modules/jooby-commons-email/pom.xml +++ b/modules/jooby-commons-email/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-commons-email jooby-commons-email diff --git a/modules/jooby-conscrypt/pom.xml b/modules/jooby-conscrypt/pom.xml index 51d54a1816..5deeb9a6d8 100644 --- a/modules/jooby-conscrypt/pom.xml +++ b/modules/jooby-conscrypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-conscrypt jooby-conscrypt diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index ea06643c1a..c19f0a776c 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-db-scheduler jooby-db-scheduler diff --git a/modules/jooby-distribution/pom.xml b/modules/jooby-distribution/pom.xml index 4c9726d594..4b16a1d559 100644 --- a/modules/jooby-distribution/pom.xml +++ b/modules/jooby-distribution/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-distribution jooby-distribution diff --git a/modules/jooby-ebean/pom.xml b/modules/jooby-ebean/pom.xml index f956649071..4e9a33afd4 100644 --- a/modules/jooby-ebean/pom.xml +++ b/modules/jooby-ebean/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-ebean jooby-ebean diff --git a/modules/jooby-flyway/pom.xml b/modules/jooby-flyway/pom.xml index 5c65e116c6..ff6f5b2f6a 100644 --- a/modules/jooby-flyway/pom.xml +++ b/modules/jooby-flyway/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-flyway jooby-flyway diff --git a/modules/jooby-freemarker/pom.xml b/modules/jooby-freemarker/pom.xml index bd72137714..3fe53adefa 100644 --- a/modules/jooby-freemarker/pom.xml +++ b/modules/jooby-freemarker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-freemarker jooby-freemarker diff --git a/modules/jooby-gradle-setup/pom.xml b/modules/jooby-gradle-setup/pom.xml index 760ccc3bef..f3484c813a 100644 --- a/modules/jooby-gradle-setup/pom.xml +++ b/modules/jooby-gradle-setup/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-gradle-setup jooby-gradle-setup diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index fa1f6fa339..e2034de259 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-graphiql jooby-graphiql diff --git a/modules/jooby-graphql/pom.xml b/modules/jooby-graphql/pom.xml index ca8504fadd..120fedd68b 100644 --- a/modules/jooby-graphql/pom.xml +++ b/modules/jooby-graphql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-graphql jooby-graphql diff --git a/modules/jooby-gson/pom.xml b/modules/jooby-gson/pom.xml index ecb107fb71..8ed17eb338 100644 --- a/modules/jooby-gson/pom.xml +++ b/modules/jooby-gson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-gson jooby-gson diff --git a/modules/jooby-guice/pom.xml b/modules/jooby-guice/pom.xml index 3727caa06d..a8a6afe8f3 100644 --- a/modules/jooby-guice/pom.xml +++ b/modules/jooby-guice/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-guice jooby-guice diff --git a/modules/jooby-handlebars/pom.xml b/modules/jooby-handlebars/pom.xml index 0be10f9bd8..5c0d7a89f8 100644 --- a/modules/jooby-handlebars/pom.xml +++ b/modules/jooby-handlebars/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-handlebars jooby-handlebars diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index d325a250c9..a1d658b579 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-hibernate-validator jooby-hibernate-validator diff --git a/modules/jooby-hibernate/pom.xml b/modules/jooby-hibernate/pom.xml index dda58e29fc..91c07d0072 100644 --- a/modules/jooby-hibernate/pom.xml +++ b/modules/jooby-hibernate/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-hibernate jooby-hibernate diff --git a/modules/jooby-hikari/pom.xml b/modules/jooby-hikari/pom.xml index 31304ca027..515a16bf56 100644 --- a/modules/jooby-hikari/pom.xml +++ b/modules/jooby-hikari/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-hikari jooby-hikari diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index 9243f83b01..80bc59429e 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jackson jooby-jackson diff --git a/modules/jooby-jackson3/pom.xml b/modules/jooby-jackson3/pom.xml index 4fc7b07efd..aa4e989af9 100644 --- a/modules/jooby-jackson3/pom.xml +++ b/modules/jooby-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jackson3 jooby-jackson3 diff --git a/modules/jooby-jasypt/pom.xml b/modules/jooby-jasypt/pom.xml index 8dfcc482cf..da4fd2dacc 100644 --- a/modules/jooby-jasypt/pom.xml +++ b/modules/jooby-jasypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jasypt jooby-jasypt diff --git a/modules/jooby-jdbi/pom.xml b/modules/jooby-jdbi/pom.xml index 9af07a768e..39dd6522f7 100644 --- a/modules/jooby-jdbi/pom.xml +++ b/modules/jooby-jdbi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jdbi jooby-jdbi diff --git a/modules/jooby-jetty/pom.xml b/modules/jooby-jetty/pom.xml index 9243dc20d1..f0026fa07f 100644 --- a/modules/jooby-jetty/pom.xml +++ b/modules/jooby-jetty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jetty jooby-jetty diff --git a/modules/jooby-jstachio/pom.xml b/modules/jooby-jstachio/pom.xml index 08a979fa18..a964fb17ff 100644 --- a/modules/jooby-jstachio/pom.xml +++ b/modules/jooby-jstachio/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jstachio jooby-jstachio diff --git a/modules/jooby-jte/pom.xml b/modules/jooby-jte/pom.xml index baa6b177fe..48aa7ccfa2 100644 --- a/modules/jooby-jte/pom.xml +++ b/modules/jooby-jte/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jte jooby-jte diff --git a/modules/jooby-jwt/pom.xml b/modules/jooby-jwt/pom.xml index ac3f3e22ac..f678afdc39 100644 --- a/modules/jooby-jwt/pom.xml +++ b/modules/jooby-jwt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-jwt jooby-jwt diff --git a/modules/jooby-kafka/pom.xml b/modules/jooby-kafka/pom.xml index 4b5bab018f..eab93e6182 100644 --- a/modules/jooby-kafka/pom.xml +++ b/modules/jooby-kafka/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-kafka jooby-kafka diff --git a/modules/jooby-kotlin/pom.xml b/modules/jooby-kotlin/pom.xml index 680e567fa2..24a195024a 100644 --- a/modules/jooby-kotlin/pom.xml +++ b/modules/jooby-kotlin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-kotlin jooby-kotlin diff --git a/modules/jooby-log4j/pom.xml b/modules/jooby-log4j/pom.xml index 82d2da3ba0..4d8d916f5a 100644 --- a/modules/jooby-log4j/pom.xml +++ b/modules/jooby-log4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-log4j jooby-log4j diff --git a/modules/jooby-logback/pom.xml b/modules/jooby-logback/pom.xml index 3650cd6387..b82e84ed39 100644 --- a/modules/jooby-logback/pom.xml +++ b/modules/jooby-logback/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-logback jooby-logback diff --git a/modules/jooby-maven-plugin/pom.xml b/modules/jooby-maven-plugin/pom.xml index c528241c9e..41844833ae 100644 --- a/modules/jooby-maven-plugin/pom.xml +++ b/modules/jooby-maven-plugin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-maven-plugin jooby-maven-plugin diff --git a/modules/jooby-metrics/pom.xml b/modules/jooby-metrics/pom.xml index b92d9d265b..49def3c1b3 100644 --- a/modules/jooby-metrics/pom.xml +++ b/modules/jooby-metrics/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-metrics jooby-metrics diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 7499ea30dd..8395662b4c 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-mutiny jooby-mutiny diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index 5494b466d3..f1e56b022c 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-netty jooby-netty diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 33002c8d5d..89fb3ca557 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-openapi jooby-openapi diff --git a/modules/jooby-pac4j/pom.xml b/modules/jooby-pac4j/pom.xml index ab62f244cb..41b609c9b8 100644 --- a/modules/jooby-pac4j/pom.xml +++ b/modules/jooby-pac4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-pac4j jooby-pac4j diff --git a/modules/jooby-pebble/pom.xml b/modules/jooby-pebble/pom.xml index e15d64ecf0..b6614d5b40 100644 --- a/modules/jooby-pebble/pom.xml +++ b/modules/jooby-pebble/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-pebble jooby-pebble diff --git a/modules/jooby-quartz/pom.xml b/modules/jooby-quartz/pom.xml index 384744501a..07ab7c2b5d 100644 --- a/modules/jooby-quartz/pom.xml +++ b/modules/jooby-quartz/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-quartz jooby-quartz diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index 5c4dfba1f9..0f07fbbd89 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-reactor jooby-reactor diff --git a/modules/jooby-redis/pom.xml b/modules/jooby-redis/pom.xml index 3c0ab9180a..4cab040da8 100644 --- a/modules/jooby-redis/pom.xml +++ b/modules/jooby-redis/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-redis jooby-redis diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index b8b8ff5c86..5e41fc411b 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-redoc jooby-redoc diff --git a/modules/jooby-rocker/pom.xml b/modules/jooby-rocker/pom.xml index 7ea9e60644..f3b894163c 100644 --- a/modules/jooby-rocker/pom.xml +++ b/modules/jooby-rocker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-rocker jooby-rocker diff --git a/modules/jooby-run/pom.xml b/modules/jooby-run/pom.xml index ad6818201a..1de34adc7f 100644 --- a/modules/jooby-run/pom.xml +++ b/modules/jooby-run/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-run jooby-run diff --git a/modules/jooby-rxjava3/pom.xml b/modules/jooby-rxjava3/pom.xml index 77293a94b1..e053dbcf5b 100644 --- a/modules/jooby-rxjava3/pom.xml +++ b/modules/jooby-rxjava3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-rxjava3 jooby-rxjava3 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index 3fb64597b0..04e7fb33e8 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -4,7 +4,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-stork diff --git a/modules/jooby-swagger-ui/pom.xml b/modules/jooby-swagger-ui/pom.xml index 0c265abf83..93935c2ffe 100644 --- a/modules/jooby-swagger-ui/pom.xml +++ b/modules/jooby-swagger-ui/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-swagger-ui jooby-swagger-ui diff --git a/modules/jooby-test/pom.xml b/modules/jooby-test/pom.xml index d68ff99fc4..0552096161 100644 --- a/modules/jooby-test/pom.xml +++ b/modules/jooby-test/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-test jooby-test diff --git a/modules/jooby-thymeleaf/pom.xml b/modules/jooby-thymeleaf/pom.xml index 9674129360..755986cba1 100644 --- a/modules/jooby-thymeleaf/pom.xml +++ b/modules/jooby-thymeleaf/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-thymeleaf jooby-thymeleaf diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index 619ed5486f..99493b903f 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-undertow jooby-undertow diff --git a/modules/jooby-vertx-mysql-client/pom.xml b/modules/jooby-vertx-mysql-client/pom.xml index 5e56a2e4b2..2fd0564a9f 100644 --- a/modules/jooby-vertx-mysql-client/pom.xml +++ b/modules/jooby-vertx-mysql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-vertx-mysql-client jooby-vertx-mysql-client diff --git a/modules/jooby-vertx-pg-client/pom.xml b/modules/jooby-vertx-pg-client/pom.xml index 00413a3841..685056b626 100644 --- a/modules/jooby-vertx-pg-client/pom.xml +++ b/modules/jooby-vertx-pg-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-vertx-pg-client jooby-vertx-pg-client diff --git a/modules/jooby-vertx-sql-client/pom.xml b/modules/jooby-vertx-sql-client/pom.xml index b548edf5a2..d7f0ac67d6 100644 --- a/modules/jooby-vertx-sql-client/pom.xml +++ b/modules/jooby-vertx-sql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-vertx-sql-client jooby-vertx-sql-client diff --git a/modules/jooby-vertx/pom.xml b/modules/jooby-vertx/pom.xml index 9a4244574f..e091423053 100644 --- a/modules/jooby-vertx/pom.xml +++ b/modules/jooby-vertx/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-vertx jooby-vertx diff --git a/modules/jooby-whoops/pom.xml b/modules/jooby-whoops/pom.xml index b0ec404392..42614abab1 100644 --- a/modules/jooby-whoops/pom.xml +++ b/modules/jooby-whoops/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-whoops jooby-whoops diff --git a/modules/jooby-yasson/pom.xml b/modules/jooby-yasson/pom.xml index 8388131438..6f9a5f4681 100644 --- a/modules/jooby-yasson/pom.xml +++ b/modules/jooby-yasson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.0.16-SNAPSHOT + 4.0.16 jooby-yasson jooby-yasson diff --git a/modules/pom.xml b/modules/pom.xml index 5277cd3326..9510e9ab68 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ io.jooby jooby-project - 4.0.16-SNAPSHOT + 4.0.16 modules diff --git a/pom.xml b/pom.xml index 2fce3125bb..6aa5a44b26 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jooby jooby-project - 4.0.16-SNAPSHOT + 4.0.16 pom jooby-project @@ -211,7 +211,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2026-01-14T14:52:09Z + 2026-03-02T16:38:34Z UTF-8 etc${file.separator}source${file.separator}formatter.sh diff --git a/tests/pom.xml b/tests/pom.xml index 0d390d96a2..9a69f4e060 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.0.16-SNAPSHOT + 4.0.16 tests tests